Offline mode (#403)

* offline mode

* fixes, mixpanels, etc

* changes

* prettier

* rev

* actions
This commit is contained in:
Wyatt Verchere
2023-08-04 19:51:46 -07:00
committed by GitHub
parent b772f916b1
commit 6a76811bed
36 changed files with 427 additions and 123 deletions

View File

@@ -233,6 +233,22 @@ pub async fn emit_warning(message: &str) -> crate::Result<()> {
Ok(()) Ok(())
} }
// emit_offline(bool)
// This is used to emit an event to the frontend that the app is offline after a refresh (or online)
#[allow(dead_code)]
#[allow(unused_variables)]
pub async fn emit_offline(offline: bool) -> crate::Result<()> {
#[cfg(feature = "tauri")]
{
let event_state = crate::EventState::get().await?;
event_state
.app
.emit_all("offline", offline)
.map_err(EventError::from)?;
}
Ok(())
}
// emit_command(CommandPayload::Something { something }) // emit_command(CommandPayload::Something { something })
// ie: installing a pack, opening an .mrpack, etc // ie: installing a pack, opening an .mrpack, etc
// Generally used for url deep links and file opens that we we want to handle in the frontend // Generally used for url deep links and file opens that we we want to handle in the frontend

View File

@@ -194,6 +194,11 @@ pub struct LoadingPayload {
pub message: String, pub message: String,
} }
#[derive(Serialize, Clone)]
pub struct OfflinePayload {
pub offline: bool,
}
#[derive(Serialize, Clone)] #[derive(Serialize, Clone)]
pub struct WarningPayload { pub struct WarningPayload {
pub message: String, pub message: String,

View File

@@ -542,7 +542,7 @@ pub async fn launch_minecraft(
} }
} }
{ if !*state.offline.read().await {
// Add game played to discord rich presence // Add game played to discord rich presence
let _ = state let _ = state
.discord_rpc .discord_rpc

View File

@@ -58,6 +58,7 @@ impl Metadata {
#[theseus_macros::debug_pin] #[theseus_macros::debug_pin]
pub async fn init( pub async fn init(
dirs: &DirectoryInfo, dirs: &DirectoryInfo,
fetch_online: bool,
io_semaphore: &IoSemaphore, io_semaphore: &IoSemaphore,
) -> crate::Result<Self> { ) -> crate::Result<Self> {
let mut metadata = None; let mut metadata = None;
@@ -67,7 +68,7 @@ impl Metadata {
read_json::<Metadata>(&metadata_path, io_semaphore).await read_json::<Metadata>(&metadata_path, io_semaphore).await
{ {
metadata = Some(metadata_json); metadata = Some(metadata_json);
} else { } else if fetch_online {
let res = async { let res = async {
let metadata_fetch = Self::fetch().await?; let metadata_fetch = Self::fetch().await?;

View File

@@ -1,16 +1,17 @@
//! Theseus state management system //! Theseus state management system
use crate::event::emit::{emit_loading, init_loading_unsafe}; use crate::event::emit::{emit_loading, emit_offline, init_loading_unsafe};
use std::path::PathBuf; use std::path::PathBuf;
use crate::event::LoadingBarType; use crate::event::LoadingBarType;
use crate::loading_join; use crate::loading_join;
use crate::state::users::Users; use crate::state::users::Users;
use crate::util::fetch::{FetchSemaphore, IoSemaphore}; use crate::util::fetch::{self, FetchSemaphore, IoSemaphore};
use notify::RecommendedWatcher; use notify::RecommendedWatcher;
use notify_debouncer_mini::{new_debouncer, DebounceEventResult, Debouncer}; use notify_debouncer_mini::{new_debouncer, DebounceEventResult, Debouncer};
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use tokio::join;
use tokio::sync::{OnceCell, RwLock, Semaphore}; use tokio::sync::{OnceCell, RwLock, Semaphore};
use futures::{channel::mpsc::channel, SinkExt, StreamExt}; use futures::{channel::mpsc::channel, SinkExt, StreamExt};
@@ -55,6 +56,9 @@ pub use self::discord::*;
// RwLock on state only has concurrent reads, except for config dir change which takes control of the State // RwLock on state only has concurrent reads, except for config dir change which takes control of the State
static LAUNCHER_STATE: OnceCell<RwLock<State>> = OnceCell::const_new(); static LAUNCHER_STATE: OnceCell<RwLock<State>> = OnceCell::const_new();
pub struct State { pub struct State {
/// Whether or not the launcher is currently operating in 'offline mode'
pub offline: RwLock<bool>,
/// Information on the location of files used in the launcher /// Information on the location of files used in the launcher
pub directories: DirectoryInfo, pub directories: DirectoryInfo,
@@ -145,10 +149,17 @@ impl State {
))); )));
emit_loading(&loading_bar, 10.0, None).await?; emit_loading(&loading_bar, 10.0, None).await?;
let metadata_fut = Metadata::init(&directories, &io_semaphore); let is_offline = !fetch::check_internet(&fetch_semaphore, 3).await;
let metadata_fut =
Metadata::init(&directories, !is_offline, &io_semaphore);
let profiles_fut = Profiles::init(&directories, &mut file_watcher); let profiles_fut = Profiles::init(&directories, &mut file_watcher);
let tags_fut = let tags_fut = Tags::init(
Tags::init(&directories, &io_semaphore, &fetch_semaphore); &directories,
!is_offline,
&io_semaphore,
&fetch_semaphore,
);
let users_fut = Users::init(&directories, &io_semaphore); let users_fut = Users::init(&directories, &io_semaphore);
// Launcher data // Launcher data
let (metadata, profiles, tags, users) = loading_join! { let (metadata, profiles, tags, users) = loading_join! {
@@ -165,9 +176,13 @@ impl State {
let discord_rpc = DiscordGuard::init().await?; let discord_rpc = DiscordGuard::init().await?;
// Starts a loop of checking if we are online, and updating
Self::offine_check_loop();
emit_loading(&loading_bar, 10.0, None).await?; emit_loading(&loading_bar, 10.0, None).await?;
Ok::<RwLock<Self>, crate::Error>(RwLock::new(Self { Ok::<RwLock<Self>, crate::Error>(RwLock::new(Self {
offline: RwLock::new(is_offline),
directories, directories,
fetch_semaphore, fetch_semaphore,
fetch_semaphore_max: RwLock::new( fetch_semaphore_max: RwLock::new(
@@ -190,13 +205,36 @@ impl State {
})) }))
} }
/// Updates state with data from the web /// Starts a loop of checking if we are online, and updating
pub fn offine_check_loop() {
tokio::task::spawn(async {
loop {
let state = Self::get().await;
if let Ok(state) = state {
let _ = state.refresh_offline().await;
}
// Wait 5 seconds
tokio::time::sleep(Duration::from_secs(5)).await;
}
});
}
/// Updates state with data from the web, if we are online
pub fn update() { pub fn update() {
tokio::task::spawn(Metadata::update()); tokio::task::spawn(async {
tokio::task::spawn(Tags::update()); if let Ok(state) = crate::State::get().await {
tokio::task::spawn(Profiles::update_projects()); if !*state.offline.read().await {
tokio::task::spawn(Profiles::update_modrinth_versions()); let res1 = Profiles::update_modrinth_versions();
tokio::task::spawn(Settings::update_java()); let res2 = Tags::update();
let res3 = Metadata::update();
let res4 = Profiles::update_projects();
let res5 = Settings::update_java();
let _ = join!(res1, res2, res3, res4, res5);
}
}
});
} }
#[tracing::instrument] #[tracing::instrument]
@@ -264,6 +302,21 @@ impl State {
*total_permits = settings.max_concurrent_downloads as u32; *total_permits = settings.max_concurrent_downloads as u32;
*io_semaphore = Semaphore::new(settings.max_concurrent_downloads); *io_semaphore = Semaphore::new(settings.max_concurrent_downloads);
} }
/// Refreshes whether or not the launcher should be offline, by whether or not there is an internet connection
pub async fn refresh_offline(&self) -> crate::Result<()> {
let is_online = fetch::check_internet(&self.fetch_semaphore, 3).await;
let mut offline = self.offline.write().await;
if *offline != is_online {
return Ok(());
}
emit_offline(!is_online).await?;
*offline = !is_online;
Ok(())
}
} }
pub async fn init_watcher() -> crate::Result<Debouncer<RecommendedWatcher>> { pub async fn init_watcher() -> crate::Result<Debouncer<RecommendedWatcher>> {

View File

@@ -24,6 +24,7 @@ impl Tags {
#[theseus_macros::debug_pin] #[theseus_macros::debug_pin]
pub async fn init( pub async fn init(
dirs: &DirectoryInfo, dirs: &DirectoryInfo,
fetch_online: bool,
io_semaphore: &IoSemaphore, io_semaphore: &IoSemaphore,
fetch_semaphore: &FetchSemaphore, fetch_semaphore: &FetchSemaphore,
) -> crate::Result<Self> { ) -> crate::Result<Self> {
@@ -33,7 +34,7 @@ impl Tags {
if let Ok(tags_json) = read_json::<Self>(&tags_path, io_semaphore).await if let Ok(tags_json) = read_json::<Self>(&tags_path, io_semaphore).await
{ {
tags = Some(tags_json); tags = Some(tags_json);
} else { } else if fetch_online {
match Self::fetch(fetch_semaphore).await { match Self::fetch(fetch_semaphore).await {
Ok(tags_fetch) => tags = Some(tags_fetch), Ok(tags_fetch) => tags = Some(tags_fetch),
Err(err) => { Err(err) => {

View File

@@ -7,7 +7,7 @@ use reqwest::Method;
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use std::ffi::OsStr; use std::ffi::OsStr;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::time; use std::time::{self, Duration};
use tokio::sync::{RwLock, Semaphore}; use tokio::sync::{RwLock, Semaphore};
use tokio::{fs::File, io::AsyncWriteExt}; use tokio::{fs::File, io::AsyncWriteExt};
@@ -182,6 +182,16 @@ pub async fn fetch_mirrors(
unreachable!() unreachable!()
} }
/// Using labrinth API, checks if an internet response can be found, with a timeout in seconds
#[tracing::instrument(skip(semaphore))]
#[theseus_macros::debug_pin]
pub async fn check_internet(semaphore: &FetchSemaphore, timeout: u64) -> bool {
let result = fetch("https://api.modrinth.com", None, semaphore);
let result =
tokio::time::timeout(Duration::from_secs(timeout), result).await;
matches!(result, Ok(Ok(_)))
}
pub async fn read_json<T>( pub async fn read_json<T>(
path: &Path, path: &Path,
semaphore: &IoSemaphore, semaphore: &IoSemaphore,

View File

@@ -18,7 +18,7 @@
"floating-vue": "^2.0.0-beta.20", "floating-vue": "^2.0.0-beta.20",
"mixpanel-browser": "^2.47.0", "mixpanel-browser": "^2.47.0",
"ofetch": "^1.0.1", "ofetch": "^1.0.1",
"omorphia": "^0.4.33", "omorphia": "^0.4.34",
"pinia": "^2.1.3", "pinia": "^2.1.3",
"qrcode.vue": "^3.4.0", "qrcode.vue": "^3.4.0",
"tauri-plugin-window-state-api": "github:tauri-apps/tauri-plugin-window-state#v1", "tauri-plugin-window-state-api": "github:tauri-apps/tauri-plugin-window-state#v1",

View File

@@ -17,8 +17,8 @@ dependencies:
specifier: ^1.0.1 specifier: ^1.0.1
version: 1.0.1 version: 1.0.1
omorphia: omorphia:
specifier: ^0.4.33 specifier: ^0.4.34
version: 0.4.33 version: 0.4.34
pinia: pinia:
specifier: ^2.1.3 specifier: ^2.1.3
version: 2.1.3(vue@3.3.4) version: 2.1.3(vue@3.3.4)
@@ -27,7 +27,7 @@ dependencies:
version: 3.4.0(vue@3.3.4) version: 3.4.0(vue@3.3.4)
tauri-plugin-window-state-api: tauri-plugin-window-state-api:
specifier: github:tauri-apps/tauri-plugin-window-state#v1 specifier: github:tauri-apps/tauri-plugin-window-state#v1
version: github.com/tauri-apps/tauri-plugin-window-state/347c792535d2623fc21f66590d06f4c8dadd85ba version: github.com/tauri-apps/tauri-plugin-window-state/5ea9eb0d4a9affd17269f92c0085935046be3f4a
vite-svg-loader: vite-svg-loader:
specifier: ^4.0.0 specifier: ^4.0.0
version: 4.0.0 version: 4.0.0
@@ -1344,8 +1344,8 @@ packages:
ufo: 1.1.2 ufo: 1.1.2
dev: false dev: false
/omorphia@0.4.33: /omorphia@0.4.34:
resolution: {integrity: sha512-Wo0t16zRL8ZLJSKVAYv6pdYhL4YXYjDYs18shO7V5cfxjcynvd5j0sui6uBR8ghVMWFEJH994AEC/urLwcHiBA==} resolution: {integrity: sha512-6uAH1kgzbYYmJDM41Vy4/MhzT9kRj+s1t8IknHKeOQqmVft+wPtv/pbA7pqTMfCzBOarLKKO5s4sNlz8TeMmaQ==}
dependencies: dependencies:
dayjs: 1.11.7 dayjs: 1.11.7
floating-vue: 2.0.0-beta.20(vue@3.3.4) floating-vue: 2.0.0-beta.20(vue@3.3.4)
@@ -1808,8 +1808,8 @@ packages:
engines: {node: '>=10'} engines: {node: '>=10'}
dev: true dev: true
github.com/tauri-apps/tauri-plugin-window-state/347c792535d2623fc21f66590d06f4c8dadd85ba: github.com/tauri-apps/tauri-plugin-window-state/5ea9eb0d4a9affd17269f92c0085935046be3f4a:
resolution: {tarball: https://codeload.github.com/tauri-apps/tauri-plugin-window-state/tar.gz/347c792535d2623fc21f66590d06f4c8dadd85ba} resolution: {tarball: https://codeload.github.com/tauri-apps/tauri-plugin-window-state/tar.gz/5ea9eb0d4a9affd17269f92c0085935046be3f4a}
name: tauri-plugin-window-state-api name: tauri-plugin-window-state-api
version: 0.0.0 version: 0.0.0
dependencies: dependencies:

View File

@@ -12,6 +12,8 @@ pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
safety_check_safe_loading_bars, safety_check_safe_loading_bars,
get_opening_command, get_opening_command,
await_sync, await_sync,
is_offline,
refresh_offline
]) ])
.build() .build()
} }
@@ -125,3 +127,20 @@ pub async fn await_sync() -> Result<()> {
tracing::debug!("State synced"); tracing::debug!("State synced");
Ok(()) Ok(())
} }
/// Check if theseus is currently in offline mode, without a refresh attempt
#[tauri::command]
pub async fn is_offline() -> Result<bool> {
let state = State::get().await?;
let offline = *state.offline.read().await;
Ok(offline)
}
/// Refreshes whether or not theseus is in offline mode, and returns the new value
#[tauri::command]
pub async fn refresh_offline() -> Result<bool> {
let state = State::get().await?;
state.refresh_offline().await?;
let offline = *state.offline.read().await;
Ok(offline)
}

View File

@@ -20,12 +20,17 @@ import RunningAppBar from '@/components/ui/RunningAppBar.vue'
import SplashScreen from '@/components/ui/SplashScreen.vue' import SplashScreen from '@/components/ui/SplashScreen.vue'
import ModrinthLoadingIndicator from '@/components/modrinth-loading-indicator' import ModrinthLoadingIndicator from '@/components/modrinth-loading-indicator'
import { useNotifications } from '@/store/notifications.js' import { useNotifications } from '@/store/notifications.js'
import { command_listener, warning_listener } from '@/helpers/events.js' import { offline_listener, command_listener, warning_listener } from '@/helpers/events.js'
import { MinimizeIcon, MaximizeIcon } from '@/assets/icons' import { MinimizeIcon, MaximizeIcon } from '@/assets/icons'
import { type } from '@tauri-apps/api/os' import { type } from '@tauri-apps/api/os'
import { appWindow } from '@tauri-apps/api/window' import { appWindow } from '@tauri-apps/api/window'
import { isDev } from '@/helpers/utils.js' import { isDev, isOffline } from '@/helpers/utils.js'
import mixpanel from 'mixpanel-browser' import {
mixpanel_track,
mixpanel_init,
mixpanel_opt_out_tracking,
mixpanel_is_loaded,
} from '@/helpers/mixpanel'
import { saveWindowState, StateFlags } from 'tauri-plugin-window-state-api' import { saveWindowState, StateFlags } from 'tauri-plugin-window-state-api'
import { getVersion } from '@tauri-apps/api/app' import { getVersion } from '@tauri-apps/api/app'
import { window as TauriWindow } from '@tauri-apps/api' import { window as TauriWindow } from '@tauri-apps/api'
@@ -33,13 +38,14 @@ import { TauriEvent } from '@tauri-apps/api/event'
import { await_sync, check_safe_loading_bars_complete } from './helpers/state' import { await_sync, check_safe_loading_bars_complete } from './helpers/state'
import { confirm } from '@tauri-apps/api/dialog' import { confirm } from '@tauri-apps/api/dialog'
import URLConfirmModal from '@/components/ui/URLConfirmModal.vue' import URLConfirmModal from '@/components/ui/URLConfirmModal.vue'
// import OnboardingScreen from '@/components/ui/tutorial/OnboardingScreen.vue'
import StickyTitleBar from '@/components/ui/tutorial/StickyTitleBar.vue' import StickyTitleBar from '@/components/ui/tutorial/StickyTitleBar.vue'
import OnboardingScreen from '@/components/ui/tutorial/OnboardingScreen.vue' import OnboardingScreen from '@/components/ui/tutorial/OnboardingScreen.vue'
const themeStore = useTheming() const themeStore = useTheming()
const urlModal = ref(null) const urlModal = ref(null)
const isLoading = ref(true) const isLoading = ref(true)
const offline = ref(false)
const videoPlaying = ref(true) const videoPlaying = ref(true)
const showOnboarding = ref(false) const showOnboarding = ref(false)
@@ -58,11 +64,11 @@ defineExpose({
themeStore.collapsedNavigation = collapsed_navigation themeStore.collapsedNavigation = collapsed_navigation
themeStore.advancedRendering = advanced_rendering themeStore.advancedRendering = advanced_rendering
mixpanel.init('014c7d6a336d0efaefe3aca91063748d', { debug: dev, persistence: 'localStorage' }) mixpanel_init('014c7d6a336d0efaefe3aca91063748d', { debug: dev, persistence: 'localStorage' })
if (opt_out_analytics) { if (opt_out_analytics) {
mixpanel.opt_out_tracking() mixpanel_opt_out_tracking()
} }
mixpanel.track('Launched', { version, dev, onboarded_new }) mixpanel_track('Launched', { version, dev, onboarded_new })
if (!dev) document.addEventListener('contextmenu', (event) => event.preventDefault()) if (!dev) document.addEventListener('contextmenu', (event) => event.preventDefault())
@@ -72,6 +78,11 @@ defineExpose({
document.getElementsByTagName('html')[0].classList.add('windows') document.getElementsByTagName('html')[0].classList.add('windows')
} }
offline.value = await isOffline()
await offline_listener((b) => {
offline.value = b
})
await warning_listener((e) => await warning_listener((e) =>
notificationsWrapper.value.addNotification({ notificationsWrapper.value.addNotification({
title: 'Warning', title: 'Warning',
@@ -121,8 +132,8 @@ TauriWindow.getCurrent().listen(TauriEvent.WINDOW_CLOSE_REQUESTED, async () => {
const router = useRouter() const router = useRouter()
router.afterEach((to, from, failure) => { router.afterEach((to, from, failure) => {
if (mixpanel.__loaded) { if (mixpanel_is_loaded()) {
mixpanel.track('PageView', { path: to.path, fromPath: from.path, failed: failure }) mixpanel_track('PageView', { path: to.path, fromPath: from.path, failed: failure })
} }
}) })
const route = useRoute() const route = useRoute()
@@ -214,6 +225,7 @@ command_listener((e) => {
<Button <Button
class="sleek-primary collapsed-button" class="sleek-primary collapsed-button"
icon-only icon-only
:disabled="offline"
@click="() => $refs.installationModal.show()" @click="() => $refs.installationModal.show()"
> >
<PlusIcon /> <PlusIcon />

View File

@@ -31,7 +31,7 @@ import { showProfileInFolder } from '@/helpers/utils.js'
import { useFetch } from '@/helpers/fetch.js' import { useFetch } from '@/helpers/fetch.js'
import { install as pack_install } from '@/helpers/pack.js' import { install as pack_install } from '@/helpers/pack.js'
import { useTheming } from '@/store/state.js' import { useTheming } from '@/store/state.js'
import mixpanel from 'mixpanel-browser' import { mixpanel_track } from '@/helpers/mixpanel'
const router = useRouter() const router = useRouter()
@@ -125,7 +125,7 @@ const handleOptionsClick = async (args) => {
switch (args.option) { switch (args.option) {
case 'play': case 'play':
await run(args.item.path).catch(handleError) await run(args.item.path).catch(handleError)
mixpanel.track('InstanceStart', { mixpanel_track('InstanceStart', {
loader: args.item.metadata.loader, loader: args.item.metadata.loader,
game_version: args.item.metadata.game_version, game_version: args.item.metadata.game_version,
}) })
@@ -134,7 +134,7 @@ const handleOptionsClick = async (args) => {
for (const u of await get_uuids_by_profile_path(args.item.path).catch(handleError)) { for (const u of await get_uuids_by_profile_path(args.item.path).catch(handleError)) {
await kill_by_uuid(u).catch(handleError) await kill_by_uuid(u).catch(handleError)
} }
mixpanel.track('InstanceStop', { mixpanel_track('InstanceStop', {
loader: args.item.metadata.loader, loader: args.item.metadata.loader,
game_version: args.item.metadata.game_version, game_version: args.item.metadata.game_version,
}) })

View File

@@ -78,7 +78,7 @@ import {
import { get, set } from '@/helpers/settings' import { get, set } from '@/helpers/settings'
import { WebviewWindow } from '@tauri-apps/api/window' import { WebviewWindow } from '@tauri-apps/api/window'
import { handleError } from '@/store/state.js' import { handleError } from '@/store/state.js'
import mixpanel from 'mixpanel-browser' import { mixpanel_track } from '@/helpers/mixpanel'
defineProps({ defineProps({
mode: { mode: {
@@ -127,7 +127,7 @@ async function login() {
await setAccount(loggedIn) await setAccount(loggedIn)
await refreshValues() await refreshValues()
await window.close() await window.close()
mixpanel.track('AccountLogIn') mixpanel_track('AccountLogIn')
} }
const logout = async (id) => { const logout = async (id) => {
@@ -139,7 +139,7 @@ const logout = async (id) => {
} else { } else {
emit('change') emit('change')
} }
mixpanel.track('AccountLogOut') mixpanel_track('AccountLogOut')
} }
let showCard = ref(false) let showCard = ref(false)

View File

@@ -59,7 +59,7 @@ import { Button, Modal, XIcon, DownloadIcon, DropdownSelect, formatCategory } fr
import { add_project_from_version as installMod } from '@/helpers/profile' import { add_project_from_version as installMod } from '@/helpers/profile'
import { ref } from 'vue' import { ref } from 'vue'
import { handleError, useTheming } from '@/store/state.js' import { handleError, useTheming } from '@/store/state.js'
import mixpanel from 'mixpanel-browser' import { mixpanel_track } from '@/helpers/mixpanel'
const themeStore = useTheming() const themeStore = useTheming()
@@ -94,7 +94,7 @@ defineExpose({
incompatibleModal.value.show() incompatibleModal.value.show()
markInstalled = extMarkInstalled markInstalled = extMarkInstalled
mixpanel.track('ProjectInstallStart', { source: 'ProjectIncompatibilityWarningModal' }) mixpanel_track('ProjectInstallStart', { source: 'ProjectIncompatibilityWarningModal' })
}, },
}) })
@@ -105,7 +105,7 @@ const install = async () => {
markInstalled() markInstalled()
incompatibleModal.value.hide() incompatibleModal.value.hide()
mixpanel.track('ProjectInstall', { mixpanel_track('ProjectInstall', {
loader: instance.value.metadata.loader, loader: instance.value.metadata.loader,
game_version: instance.value.metadata.game_version, game_version: instance.value.metadata.game_version,
id: project.value, id: project.value,

View File

@@ -2,7 +2,7 @@
import { Button, Modal, XIcon, DownloadIcon } from 'omorphia' import { Button, Modal, XIcon, DownloadIcon } from 'omorphia'
import { install as pack_install } from '@/helpers/pack' import { install as pack_install } from '@/helpers/pack'
import { ref } from 'vue' import { ref } from 'vue'
import mixpanel from 'mixpanel-browser' import { mixpanel_track } from '@/helpers/mixpanel'
import { useTheming } from '@/store/theme.js' import { useTheming } from '@/store/theme.js'
import { handleError } from '@/store/state.js' import { handleError } from '@/store/state.js'
@@ -24,7 +24,7 @@ defineExpose({
installing.value = false installing.value = false
confirmModal.value.show() confirmModal.value.show()
mixpanel.track('PackInstallStart') mixpanel_track('PackInstallStart')
}, },
}) })
@@ -38,7 +38,7 @@ async function install() {
title.value, title.value,
icon.value ? icon.value : null icon.value ? icon.value : null
).catch(handleError) ).catch(handleError)
mixpanel.track('PackInstall', { mixpanel_track('PackInstall', {
id: projectId.value, id: projectId.value,
version_id: version.value, version_id: version.value,
title: title.value, title: title.value,

View File

@@ -16,7 +16,7 @@ import { useFetch } from '@/helpers/fetch.js'
import { handleError } from '@/store/state.js' import { handleError } from '@/store/state.js'
import { showProfileInFolder } from '@/helpers/utils.js' import { showProfileInFolder } from '@/helpers/utils.js'
import ModInstallModal from '@/components/ui/ModInstallModal.vue' import ModInstallModal from '@/components/ui/ModInstallModal.vue'
import mixpanel from 'mixpanel-browser' import { mixpanel_track } from '@/helpers/mixpanel'
const props = defineProps({ const props = defineProps({
instance: { instance: {
@@ -93,7 +93,7 @@ const install = async (e) => {
).catch(handleError) ).catch(handleError)
modLoading.value = false modLoading.value = false
mixpanel.track('PackInstall', { mixpanel_track('PackInstall', {
id: props.instance.project_id, id: props.instance.project_id,
version_id: versions[0].id, version_id: versions[0].id,
title: props.instance.title, title: props.instance.title,
@@ -125,7 +125,7 @@ const play = async (e, context) => {
modLoading.value = false modLoading.value = false
playing.value = true playing.value = true
mixpanel.track('InstancePlay', { mixpanel_track('InstancePlay', {
loader: props.instance.metadata.loader, loader: props.instance.metadata.loader,
game_version: props.instance.metadata.game_version, game_version: props.instance.metadata.game_version,
source: context, source: context,
@@ -145,7 +145,7 @@ const stop = async (e, context) => {
uuids.forEach(async (u) => await kill_by_uuid(u).catch(handleError)) uuids.forEach(async (u) => await kill_by_uuid(u).catch(handleError))
} else await kill_by_uuid(uuid.value).catch(handleError) // If we still have the uuid, just kill it } else await kill_by_uuid(uuid.value).catch(handleError) // If we still have the uuid, just kill it
mixpanel.track('InstanceStop', { mixpanel_track('InstanceStop', {
loader: props.instance.metadata.loader, loader: props.instance.metadata.loader,
game_version: props.instance.metadata.game_version, game_version: props.instance.metadata.game_version,
source: context, source: context,

View File

@@ -215,7 +215,7 @@ import {
} from '@/helpers/metadata' } from '@/helpers/metadata'
import { handleError } from '@/store/notifications.js' import { handleError } from '@/store/notifications.js'
import Multiselect from 'vue-multiselect' import Multiselect from 'vue-multiselect'
import mixpanel from 'mixpanel-browser' import { mixpanel_track } from '@/helpers/mixpanel'
import { useTheming } from '@/store/state.js' import { useTheming } from '@/store/state.js'
import { listen } from '@tauri-apps/api/event' import { listen } from '@tauri-apps/api/event'
import { install_from_file } from '@/helpers/pack.js' import { install_from_file } from '@/helpers/pack.js'
@@ -249,7 +249,7 @@ defineExpose({
display_icon.value = null display_icon.value = null
modal.value.show() modal.value.show()
mixpanel.track('InstanceCreateStart', { source: 'CreationModal' }) mixpanel_track('InstanceCreateStart', { source: 'CreationModal' })
}, },
}) })
@@ -314,7 +314,7 @@ const create_instance = async () => {
icon.value icon.value
).catch(handleError) ).catch(handleError)
mixpanel.track('InstanceCreate', { mixpanel_track('InstanceCreate', {
profile_name: profile_name.value, profile_name: profile_name.value,
game_version: game_version.value, game_version: game_version.value,
loader: loader.value, loader: loader.value,
@@ -370,7 +370,7 @@ const openFile = async () => {
modal.value.hide() modal.value.hide()
await install_from_file(newProject).catch(handleError) await install_from_file(newProject).catch(handleError)
mixpanel.track('InstanceCreate', { mixpanel_track('InstanceCreate', {
source: 'CreationModalFileOpen', source: 'CreationModalFileOpen',
}) })
} }
@@ -379,7 +379,7 @@ listen('tauri://file-drop', async (event) => {
modal.value.hide() modal.value.hide()
if (event.payload && event.payload.length > 0 && event.payload[0].endsWith('.mrpack')) { if (event.payload && event.payload.length > 0 && event.payload[0].endsWith('.mrpack')) {
await install_from_file(event.payload[0]).catch(handleError) await install_from_file(event.payload[0]).catch(handleError)
mixpanel.track('InstanceCreate', { mixpanel_track('InstanceCreate', {
source: 'CreationModalFileDrop', source: 'CreationModalFileDrop',
}) })
} }

View File

@@ -44,7 +44,7 @@ import {
get_all_jre, get_all_jre,
} from '@/helpers/jre.js' } from '@/helpers/jre.js'
import { handleError } from '@/store/notifications.js' import { handleError } from '@/store/notifications.js'
import mixpanel from 'mixpanel-browser' import { mixpanel_track } from '@/helpers/mixpanel'
import { useTheming } from '@/store/theme.js' import { useTheming } from '@/store/theme.js'
const themeStore = useTheming() const themeStore = useTheming()
@@ -79,7 +79,7 @@ const emit = defineEmits(['submit'])
function setJavaInstall(javaInstall) { function setJavaInstall(javaInstall) {
emit('submit', javaInstall) emit('submit', javaInstall)
detectJavaModal.value.hide() detectJavaModal.value.hide()
mixpanel.track('JavaAutoDetect', { mixpanel_track('JavaAutoDetect', {
path: javaInstall.path, path: javaInstall.path,
version: javaInstall.version, version: javaInstall.version,
}) })

View File

@@ -49,7 +49,7 @@ import { find_jre_17_jres, get_jre } from '@/helpers/jre.js'
import { ref } from 'vue' import { ref } from 'vue'
import { open } from '@tauri-apps/api/dialog' import { open } from '@tauri-apps/api/dialog'
import JavaDetectionModal from '@/components/ui/JavaDetectionModal.vue' import JavaDetectionModal from '@/components/ui/JavaDetectionModal.vue'
import mixpanel from 'mixpanel-browser' import { mixpanel_track } from '@/helpers/mixpanel'
import { handleError } from '@/store/state.js' import { handleError } from '@/store/state.js'
const props = defineProps({ const props = defineProps({
@@ -88,7 +88,7 @@ async function testJava() {
testingJava.value = false testingJava.value = false
testingJavaSuccess.value = !!result testingJavaSuccess.value = !!result
mixpanel.track('JavaTest', { mixpanel_track('JavaTest', {
path: props.modelValue ? props.modelValue.path : '', path: props.modelValue ? props.modelValue.path : '',
success: !!result, success: !!result,
}) })
@@ -110,7 +110,7 @@ async function handleJavaFileInput() {
architecture: 'x86', architecture: 'x86',
} }
mixpanel.track('JavaManualSelect', { mixpanel_track('JavaManualSelect', {
path: filePath, path: filePath,
version: props.version, version: props.version,
}) })

View File

@@ -22,7 +22,7 @@ import { open } from '@tauri-apps/api/dialog'
import { create } from '@/helpers/profile' import { create } from '@/helpers/profile'
import { installVersionDependencies } from '@/helpers/utils' import { installVersionDependencies } from '@/helpers/utils'
import { handleError } from '@/store/notifications.js' import { handleError } from '@/store/notifications.js'
import mixpanel from 'mixpanel-browser' import { mixpanel_track } from '@/helpers/mixpanel'
import { useTheming } from '@/store/theme.js' import { useTheming } from '@/store/theme.js'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { tauri } from '@tauri-apps/api' import { tauri } from '@tauri-apps/api'
@@ -57,7 +57,7 @@ defineExpose({
profiles.value = await getData() profiles.value = await getData()
mixpanel.track('ProjectInstallStart', { source: 'ProjectInstallModal' }) mixpanel_track('ProjectInstallStart', { source: 'ProjectInstallModal' })
}, },
}) })
@@ -88,7 +88,7 @@ async function install(instance) {
instance.installedMod = true instance.installedMod = true
instance.installing = false instance.installing = false
mixpanel.track('ProjectInstall', { mixpanel_track('ProjectInstall', {
loader: instance.metadata.loader, loader: instance.metadata.loader,
game_version: instance.metadata.game_version, game_version: instance.metadata.game_version,
id: project.value, id: project.value,
@@ -137,7 +137,7 @@ const toggleCreation = () => {
if (!alreadySentCreation.value) { if (!alreadySentCreation.value) {
alreadySentCreation.value = false alreadySentCreation.value = false
mixpanel.track('InstanceCreateStart', { source: 'ProjectInstallModal' }) mixpanel_track('InstanceCreateStart', { source: 'ProjectInstallModal' })
} }
} }
@@ -186,7 +186,7 @@ const createInstance = async () => {
const instance = await get(id, true) const instance = await get(id, true)
await installVersionDependencies(instance, versions.value) await installVersionDependencies(instance, versions.value)
mixpanel.track('InstanceCreate', { mixpanel_track('InstanceCreate', {
profile_name: name.value, profile_name: name.value,
game_version: versions.value[0].game_versions[0], game_version: versions.value[0].game_versions[0],
loader: loader, loader: loader,
@@ -195,7 +195,7 @@ const createInstance = async () => {
source: 'ProjectInstallModal', source: 'ProjectInstallModal',
}) })
mixpanel.track('ProjectInstall', { mixpanel_track('ProjectInstall', {
loader: loader, loader: loader,
game_version: versions.value[0].game_versions[0], game_version: versions.value[0].game_versions[0],
id: project.value, id: project.value,

View File

@@ -9,6 +9,12 @@
> >
<DownloadIcon /> <DownloadIcon />
</Button> </Button>
<div v-if="offline" class="status">
<span class="circle stopped" />
<div class="running-text clickable" @click="refreshInternet()">
<span> Offline </span>
</div>
</div>
<div v-if="selectedProfile" class="status"> <div v-if="selectedProfile" class="status">
<span class="circle running" /> <span class="circle running" />
<div ref="profileButton" class="running-text"> <div ref="profileButton" class="running-text">
@@ -107,12 +113,13 @@ import {
kill_by_uuid as killProfile, kill_by_uuid as killProfile,
get_uuids_by_profile_path as getProfileProcesses, get_uuids_by_profile_path as getProfileProcesses,
} from '@/helpers/process' } from '@/helpers/process'
import { loading_listener, process_listener } from '@/helpers/events' import { loading_listener, process_listener, offline_listener } from '@/helpers/events'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { progress_bars_list } from '@/helpers/state.js' import { progress_bars_list } from '@/helpers/state.js'
import { refreshOffline, isOffline } from '@/helpers/utils.js'
import ProgressBar from '@/components/ui/ProgressBar.vue' import ProgressBar from '@/components/ui/ProgressBar.vue'
import { handleError } from '@/store/notifications.js' import { handleError } from '@/store/notifications.js'
import mixpanel from 'mixpanel-browser' import { mixpanel_track } from '@/helpers/mixpanel'
const router = useRouter() const router = useRouter()
const card = ref(null) const card = ref(null)
@@ -126,10 +133,20 @@ const showProfiles = ref(false)
const currentProcesses = ref(await getRunningProfiles().catch(handleError)) const currentProcesses = ref(await getRunningProfiles().catch(handleError))
const selectedProfile = ref(currentProcesses.value[0]) const selectedProfile = ref(currentProcesses.value[0])
const offline = ref(await isOffline().catch(handleError))
const refreshInternet = async () => {
offline.value = await refreshOffline().catch(handleError)
}
const unlistenProcess = await process_listener(async () => { const unlistenProcess = await process_listener(async () => {
await refresh() await refresh()
}) })
const unlistenRefresh = await offline_listener(async (b) => {
offline.value = b
await refresh()
})
const refresh = async () => { const refresh = async () => {
currentProcesses.value = await getRunningProfiles().catch(handleError) currentProcesses.value = await getRunningProfiles().catch(handleError)
if (!currentProcesses.value.includes(selectedProfile.value)) { if (!currentProcesses.value.includes(selectedProfile.value)) {
@@ -142,7 +159,7 @@ const stop = async (path) => {
const processes = await getProfileProcesses(path ?? selectedProfile.value.path) const processes = await getProfileProcesses(path ?? selectedProfile.value.path)
await killProfile(processes[0]) await killProfile(processes[0])
mixpanel.track('InstanceStop', { mixpanel_track('InstanceStop', {
loader: currentProcesses.value[0].metadata.loader, loader: currentProcesses.value[0].metadata.loader,
game_version: currentProcesses.value[0].metadata.game_version, game_version: currentProcesses.value[0].metadata.game_version,
source: 'AppBar', source: 'AppBar',
@@ -240,6 +257,7 @@ onBeforeUnmount(() => {
window.removeEventListener('click', handleClickOutsideProfile) window.removeEventListener('click', handleClickOutsideProfile)
unlistenProcess() unlistenProcess()
unlistenLoading() unlistenLoading()
unlistenRefresh()
}) })
</script> </script>

View File

@@ -85,7 +85,7 @@ import { install as packInstall } from '@/helpers/pack.js'
import { installVersionDependencies } from '@/helpers/utils.js' import { installVersionDependencies } from '@/helpers/utils.js'
import { useFetch } from '@/helpers/fetch.js' import { useFetch } from '@/helpers/fetch.js'
import { handleError } from '@/store/notifications.js' import { handleError } from '@/store/notifications.js'
import mixpanel from 'mixpanel-browser' import { mixpanel_track } from '@/helpers/mixpanel'
dayjs.extend(relativeTime) dayjs.extend(relativeTime)
const props = defineProps({ const props = defineProps({
@@ -165,7 +165,7 @@ async function install() {
props.project.icon_url props.project.icon_url
).catch(handleError) ).catch(handleError)
mixpanel.track('PackInstall', { mixpanel_track('PackInstall', {
id: props.project.project_id, id: props.project.project_id,
version_id: queuedVersionData.id, version_id: queuedVersionData.id,
title: props.project.title, title: props.project.title,
@@ -196,7 +196,7 @@ async function install() {
await installMod(props.instance.path, queuedVersionData.id).catch(handleError) await installMod(props.instance.path, queuedVersionData.id).catch(handleError)
await installVersionDependencies(props.instance, queuedVersionData) await installVersionDependencies(props.instance, queuedVersionData)
mixpanel.track('ProjectInstall', { mixpanel_track('ProjectInstall', {
loader: props.instance.metadata.loader, loader: props.instance.metadata.loader,
game_version: props.instance.metadata.game_version, game_version: props.instance.metadata.game_version,
id: props.project.project_id, id: props.project.project_id,

View File

@@ -92,3 +92,15 @@ export async function command_listener(callback) {
export async function warning_listener(callback) { export async function warning_listener(callback) {
return await listen('warning', (event) => callback(event.payload)) return await listen('warning', (event) => callback(event.payload))
} }
/// Payload for the 'offline' event
/*
OfflinePayload {
offline: bool, true or false
}
*/
export async function offline_listener(callback) {
return await listen('offline', (event) => {
return callback(event.payload)
})
}

View File

@@ -0,0 +1,57 @@
import mixpanel from 'mixpanel-browser'
// mixpanel_track
function trackWrapper(originalTrack) {
return function (event_name, properties = {}) {
try {
originalTrack(event_name, properties)
} catch (e) {
console.error(e)
}
}
}
export const mixpanel_track = trackWrapper(mixpanel.track.bind(mixpanel))
// mixpanel_opt_out_tracking()
function optOutTrackingWrapper(originalOptOutTracking) {
return function () {
try {
originalOptOutTracking()
} catch (e) {
console.error(e)
}
}
}
export const mixpanel_opt_out_tracking = optOutTrackingWrapper(
mixpanel.opt_out_tracking.bind(mixpanel)
)
// mixpanel_opt_in_tracking()
function optInTrackingWrapper(originalOptInTracking) {
return function () {
try {
originalOptInTracking()
} catch (e) {
console.error(e)
}
}
}
export const mixpanel_opt_in_tracking = optInTrackingWrapper(
mixpanel.opt_in_tracking.bind(mixpanel)
)
// mixpanel_init
function initWrapper(originalInit) {
return function (token, config = {}) {
try {
originalInit(token, config)
} catch (e) {
console.error(e)
}
}
}
export const mixpanel_init = initWrapper(mixpanel.init.bind(mixpanel))
export const mixpanel_is_loaded = () => {
return mixpanel.__loaded
}

View File

@@ -77,3 +77,12 @@ export const openLink = (url) => {
}, },
}) })
} }
export const refreshOffline = async () => {
return await invoke('plugin:utils|refresh_offline', {})
}
// returns true/false
export const isOffline = async () => {
return await invoke('plugin:utils|is_offline', {})
}

View File

@@ -1,5 +1,5 @@
<script setup> <script setup>
import { computed, nextTick, ref, readonly, shallowRef, watch } from 'vue' import { computed, nextTick, ref, readonly, shallowRef, watch, onUnmounted } from 'vue'
import { import {
Pagination, Pagination,
Checkbox, Checkbox,
@@ -31,10 +31,17 @@ import IncompatibilityWarningModal from '@/components/ui/IncompatibilityWarningM
import { useFetch } from '@/helpers/fetch.js' import { useFetch } from '@/helpers/fetch.js'
import { check_installed, get as getInstance } from '@/helpers/profile.js' import { check_installed, get as getInstance } from '@/helpers/profile.js'
import { convertFileSrc } from '@tauri-apps/api/tauri' import { convertFileSrc } from '@tauri-apps/api/tauri'
import { isOffline } from '@/helpers/utils'
import { offline_listener } from '@/helpers/events'
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
const offline = ref(await isOffline())
const unlistenOffline = await offline_listener((b) => {
offline.value = b
})
const confirmModal = ref(null) const confirmModal = ref(null)
const modInstallModal = ref(null) const modInstallModal = ref(null)
const incompatibilityWarningModal = ref(null) const incompatibilityWarningModal = ref(null)
@@ -498,10 +505,12 @@ const showLoaders = computed(
instanceContext.value === null) || instanceContext.value === null) ||
ignoreInstanceLoaders.value ignoreInstanceLoaders.value
) )
onUnmounted(() => unlistenOffline())
</script> </script>
<template> <template>
<div ref="searchWrapper" class="search-container"> <div v-if="!offline" ref="searchWrapper" class="search-container">
<aside class="filter-panel"> <aside class="filter-panel">
<Card v-if="instanceContext" class="small-instance"> <Card v-if="instanceContext" class="small-instance">
<router-link :to="`/instance/${encodeURIComponent(instanceContext.path)}`" class="instance"> <router-link :to="`/instance/${encodeURIComponent(instanceContext.path)}`" class="instance">
@@ -695,7 +704,7 @@ const showLoaders = computed(
class="pagination-before" class="pagination-before"
@switch-page="onSearchChange" @switch-page="onSearchChange"
/> />
<SplashScreen v-if="loading" /> <SplashScreen v-if="loading || offline" />
<section v-else class="project-list display-mode--list instance-results" role="list"> <section v-else class="project-list display-mode--list instance-results" role="list">
<SearchCard <SearchCard
v-for="result in results.hits" v-for="result in results.hits"

View File

@@ -1,13 +1,14 @@
<script setup> <script setup>
import { ref, onUnmounted, shallowRef } from 'vue' import { ref, onUnmounted, shallowRef, computed } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import RowDisplay from '@/components/RowDisplay.vue' import RowDisplay from '@/components/RowDisplay.vue'
import { list } from '@/helpers/profile.js' import { list } from '@/helpers/profile.js'
import { profile_listener } from '@/helpers/events' import { offline_listener, profile_listener } from '@/helpers/events'
import { useBreadcrumbs } from '@/store/breadcrumbs' import { useBreadcrumbs } from '@/store/breadcrumbs'
import { useFetch } from '@/helpers/fetch.js' import { useFetch } from '@/helpers/fetch.js'
import { handleError } from '@/store/notifications.js' import { handleError } from '@/store/notifications.js'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { isOffline } from '@/helpers/utils'
const featuredModpacks = ref({}) const featuredModpacks = ref({})
const featuredMods = ref({}) const featuredMods = ref({})
@@ -20,6 +21,8 @@ breadcrumbs.setRootContext({ name: 'Home', link: route.path })
const recentInstances = shallowRef([]) const recentInstances = shallowRef([])
const offline = ref(await isOffline())
const getInstances = async () => { const getInstances = async () => {
const profiles = await list(true).catch(handleError) const profiles = await list(true).catch(handleError)
recentInstances.value = Object.values(profiles).sort((a, b) => { recentInstances.value = Object.values(profiles).sort((a, b) => {
@@ -40,32 +43,55 @@ const getFeaturedModpacks = async () => {
`https://api.modrinth.com/v2/search?facets=[["project_type:modpack"]]&limit=10&index=follows&filters=${filter.value}`, `https://api.modrinth.com/v2/search?facets=[["project_type:modpack"]]&limit=10&index=follows&filters=${filter.value}`,
'featured modpacks' 'featured modpacks'
) )
featuredModpacks.value = response.hits if (response) featuredModpacks.value = response.hits
} }
const getFeaturedMods = async () => { const getFeaturedMods = async () => {
const response = await useFetch( const response = await useFetch(
'https://api.modrinth.com/v2/search?facets=[["project_type:mod"]]&limit=10&index=follows', 'https://api.modrinth.com/v2/search?facets=[["project_type:mod"]]&limit=10&index=follows',
'featured mods' 'featured mods'
) )
featuredMods.value = response.hits if (response) featuredMods.value = response.hits
} }
await getInstances() await getInstances()
await Promise.all([getFeaturedModpacks(), getFeaturedMods()])
const unlisten = await profile_listener(async (e) => { if (!offline.value) {
await Promise.all([getFeaturedModpacks(), getFeaturedMods()])
}
const unlistenProfile = await profile_listener(async (e) => {
await getInstances() await getInstances()
if (e.event === 'created' || e.event === 'removed') { if (e.event === 'created' || e.event === 'removed') {
await Promise.all([getFeaturedModpacks(), getFeaturedMods()]) await Promise.all([getFeaturedModpacks(), getFeaturedMods()])
} }
}) })
onUnmounted(() => unlisten()) const unlistenOffline = await offline_listener(async (b) => {
offline.value = b
if (!b) {
await Promise.all([getFeaturedModpacks(), getFeaturedMods()])
}
})
// computed sums of recentInstances, featuredModpacks, featuredMods, treating them as arrays if they are not
const total = computed(() => {
return (
(recentInstances.value?.length ?? 0) +
(featuredModpacks.value?.length ?? 0) +
(featuredMods.value?.length ?? 0)
)
})
onUnmounted(() => {
unlistenProfile()
unlistenOffline()
})
</script> </script>
<template> <template>
<div class="page-container"> <div class="page-container">
<RowDisplay <RowDisplay
v-if="total > 0"
:instances="[ :instances="[
{ {
label: 'Jump back in', label: 'Jump back in',

View File

@@ -1,14 +1,15 @@
<script setup> <script setup>
import { onUnmounted, shallowRef } from 'vue' import { onUnmounted, ref, shallowRef } from 'vue'
import GridDisplay from '@/components/GridDisplay.vue' import GridDisplay from '@/components/GridDisplay.vue'
import { list } from '@/helpers/profile.js' import { list } from '@/helpers/profile.js'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import { useBreadcrumbs } from '@/store/breadcrumbs' import { useBreadcrumbs } from '@/store/breadcrumbs'
import { profile_listener } from '@/helpers/events.js' import { offline_listener, profile_listener } from '@/helpers/events.js'
import { handleError } from '@/store/notifications.js' import { handleError } from '@/store/notifications.js'
import { Button, PlusIcon } from 'omorphia' import { Button, PlusIcon } from 'omorphia'
import InstanceCreationModal from '@/components/ui/InstanceCreationModal.vue' import InstanceCreationModal from '@/components/ui/InstanceCreationModal.vue'
import { NewInstanceImage } from '@/assets/icons' import { NewInstanceImage } from '@/assets/icons'
import { isOffline } from '@/helpers/utils'
const route = useRoute() const route = useRoute()
const breadcrumbs = useBreadcrumbs() const breadcrumbs = useBreadcrumbs()
@@ -18,11 +19,19 @@ breadcrumbs.setRootContext({ name: 'Library', link: route.path })
const profiles = await list(true).catch(handleError) const profiles = await list(true).catch(handleError)
const instances = shallowRef(Object.values(profiles)) const instances = shallowRef(Object.values(profiles))
const unlisten = await profile_listener(async () => { const offline = ref(await isOffline())
const unlistenOffline = await offline_listener((b) => {
offline.value = b
})
const unlistenProfile = await profile_listener(async () => {
const profiles = await list(true).catch(handleError) const profiles = await list(true).catch(handleError)
instances.value = Object.values(profiles) instances.value = Object.values(profiles)
}) })
onUnmounted(() => unlisten()) onUnmounted(() => {
unlistenProfile()
unlistenOffline()
})
</script> </script>
<template> <template>
@@ -37,7 +46,7 @@ onUnmounted(() => unlisten())
<NewInstanceImage /> <NewInstanceImage />
</div> </div>
<h3>No instances found</h3> <h3>No instances found</h3>
<Button color="primary" @click="$refs.installationModal.show()"> <Button color="primary" :disabled="offline" @click="$refs.installationModal.show()">
<PlusIcon /> <PlusIcon />
Create new instance Create new instance
</Button> </Button>

View File

@@ -5,7 +5,7 @@ import { handleError, useTheming } from '@/store/state'
import { get, set } from '@/helpers/settings' import { get, set } from '@/helpers/settings'
import { get_max_memory } from '@/helpers/jre' import { get_max_memory } from '@/helpers/jre'
import JavaSelector from '@/components/ui/JavaSelector.vue' import JavaSelector from '@/components/ui/JavaSelector.vue'
import mixpanel from 'mixpanel-browser' import { mixpanel_opt_out_tracking, mixpanel_opt_in_tracking } from '@/helpers/mixpanel'
const pageOptions = ['Home', 'Library'] const pageOptions = ['Home', 'Library']
@@ -30,9 +30,9 @@ watch(
const setSettings = JSON.parse(JSON.stringify(newSettings)) const setSettings = JSON.parse(JSON.stringify(newSettings))
if (setSettings.opt_out_analytics) { if (setSettings.opt_out_analytics) {
mixpanel.opt_out_tracking() mixpanel_opt_out_tracking()
} else { } else {
mixpanel.opt_in_tracking() mixpanel_opt_in_tracking()
} }
if (setSettings.java_globals.JAVA_8?.path === '') { if (setSettings.java_globals.JAVA_8?.path === '') {

View File

@@ -86,7 +86,12 @@
<RouterView v-slot="{ Component }"> <RouterView v-slot="{ Component }">
<template v-if="Component"> <template v-if="Component">
<Suspense @pending="loadingBar.startLoading()" @resolve="loadingBar.stopLoading()"> <Suspense @pending="loadingBar.startLoading()" @resolve="loadingBar.stopLoading()">
<component :is="Component" :instance="instance" :options="options"></component> <component
:is="Component"
:instance="instance"
:options="options"
:offline="offline"
></component>
</Suspense> </Suspense>
</template> </template>
</RouterView> </RouterView>
@@ -144,13 +149,13 @@ import {
get_uuids_by_profile_path, get_uuids_by_profile_path,
kill_by_uuid, kill_by_uuid,
} from '@/helpers/process' } from '@/helpers/process'
import { process_listener, profile_listener } from '@/helpers/events' import { offline_listener, process_listener, profile_listener } from '@/helpers/events'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { ref, onUnmounted } from 'vue' import { ref, onUnmounted } from 'vue'
import { handleError, useBreadcrumbs, useLoading } from '@/store/state' import { handleError, useBreadcrumbs, useLoading } from '@/store/state'
import { showProfileInFolder } from '@/helpers/utils.js' import { isOffline, showProfileInFolder } from '@/helpers/utils.js'
import ContextMenu from '@/components/ui/ContextMenu.vue' import ContextMenu from '@/components/ui/ContextMenu.vue'
import mixpanel from 'mixpanel-browser' import { mixpanel_track } from '@/helpers/mixpanel'
import { PackageIcon } from '@/assets/icons/index.js' import { PackageIcon } from '@/assets/icons/index.js'
import ExportModal from '@/components/ui/ExportModal.vue' import ExportModal from '@/components/ui/ExportModal.vue'
import { convertFileSrc } from '@tauri-apps/api/tauri' import { convertFileSrc } from '@tauri-apps/api/tauri'
@@ -170,6 +175,8 @@ breadcrumbs.setContext({
query: route.query, query: route.query,
}) })
const offline = ref(await isOffline())
const loadingBar = useLoading() const loadingBar = useLoading()
const uuid = ref(null) const uuid = ref(null)
@@ -183,7 +190,7 @@ const startInstance = async (context) => {
loading.value = false loading.value = false
playing.value = true playing.value = true
mixpanel.track('InstanceStart', { mixpanel_track('InstanceStart', {
loader: instance.value.metadata.loader, loader: instance.value.metadata.loader,
game_version: instance.value.metadata.game_version, game_version: instance.value.metadata.game_version,
source: context, source: context,
@@ -211,7 +218,7 @@ const stopInstance = async (context) => {
uuids.forEach(async (u) => await kill_by_uuid(u).catch(handleError)) uuids.forEach(async (u) => await kill_by_uuid(u).catch(handleError))
} else await kill_by_uuid(uuid.value).catch(handleError) } else await kill_by_uuid(uuid.value).catch(handleError)
mixpanel.track('InstanceStop', { mixpanel_track('InstanceStop', {
loader: instance.value.metadata.loader, loader: instance.value.metadata.loader,
game_version: instance.value.metadata.game_version, game_version: instance.value.metadata.game_version,
source: context, source: context,
@@ -292,9 +299,14 @@ const unlistenProcesses = await process_listener((e) => {
if (e.event === 'finished' && uuid.value === e.uuid) playing.value = false if (e.event === 'finished' && uuid.value === e.uuid) playing.value = false
}) })
const unlistenOffline = await offline_listener((b) => {
offline.value = b
})
onUnmounted(() => { onUnmounted(() => {
unlistenProcesses() unlistenProcesses()
unlistenProfiles() unlistenProfiles()
unlistenOffline()
}) })
</script> </script>

View File

@@ -15,7 +15,7 @@
<CheckIcon v-else /> <CheckIcon v-else />
{{ copied ? 'Copied' : 'Copy' }} {{ copied ? 'Copied' : 'Copy' }}
</Button> </Button>
<Button color="primary" @click="share"> <Button color="primary" :disabled="offline" @click="share">
<ShareIcon /> <ShareIcon />
Share Share
</Button> </Button>
@@ -78,6 +78,10 @@ const props = defineProps({
type: Object, type: Object,
required: true, required: true,
}, },
offline: {
type: Boolean,
default: false,
},
}) })
const logs = ref([]) const logs = ref([])

View File

@@ -93,6 +93,7 @@
</Button> </Button>
<Button <Button
class="transparent update" class="transparent update"
:disabled="offline"
@click="updateAll()" @click="updateAll()"
@mouseover="selectedOption = 'Update'" @mouseover="selectedOption = 'Update'"
> >
@@ -139,7 +140,7 @@
</Button> </Button>
</section> </section>
<section v-if="selectedOption === 'Update'" class="options"> <section v-if="selectedOption === 'Update'" class="options">
<Button class="transparent" @click="updateAll()"> <Button class="transparent" :disabled="offline" @click="updateAll()">
<UpdatedIcon /> <UpdatedIcon />
Update all Update all
</Button> </Button>
@@ -181,6 +182,7 @@
<router-link <router-link
v-if="mod.slug" v-if="mod.slug"
:to="{ path: `/project/${mod.slug}/`, query: { i: props.instance.path } }" :to="{ path: `/project/${mod.slug}/`, query: { i: props.instance.path } }"
:disabled="offline"
class="mod-content" class="mod-content"
> >
<Avatar :src="mod.icon" /> <Avatar :src="mod.icon" />
@@ -208,7 +210,7 @@
<Button <Button
v-else v-else
v-tooltip="'Update project'" v-tooltip="'Update project'"
:disabled="!mod.outdated" :disabled="!mod.outdated || offline"
icon-only icon-only
@click="updateProject(mod)" @click="updateProject(mod)"
> >
@@ -345,7 +347,7 @@ import {
update_project, update_project,
} from '@/helpers/profile.js' } from '@/helpers/profile.js'
import { handleError } from '@/store/notifications.js' import { handleError } from '@/store/notifications.js'
import mixpanel from 'mixpanel-browser' import { mixpanel_track } from '@/helpers/mixpanel'
import { open } from '@tauri-apps/api/dialog' import { open } from '@tauri-apps/api/dialog'
import { listen } from '@tauri-apps/api/event' import { listen } from '@tauri-apps/api/event'
import { convertFileSrc } from '@tauri-apps/api/tauri' import { convertFileSrc } from '@tauri-apps/api/tauri'
@@ -368,6 +370,12 @@ const props = defineProps({
return {} return {}
}, },
}, },
offline: {
type: Boolean,
default() {
return false
},
},
}) })
const projects = ref([]) const projects = ref([])
@@ -376,8 +384,9 @@ const showingOptions = ref(false)
const initProjects = (initInstance) => { const initProjects = (initInstance) => {
projects.value = [] projects.value = []
if (!initInstance || !initInstance.projects) return
for (const [path, project] of Object.entries(initInstance.projects)) { for (const [path, project] of Object.entries(initInstance.projects)) {
if (project.metadata.type === 'modrinth') { if (project.metadata.type === 'modrinth' && !props.offline) {
let owner = project.metadata.members.find((x) => x.role === 'Owner') let owner = project.metadata.members.find((x) => x.role === 'Owner')
projects.value.push({ projects.value.push({
path, path,
@@ -442,6 +451,13 @@ watch(
} }
) )
watch(
() => props.offline,
() => {
if (props.instance) initProjects(props.instance)
}
)
const searchFilter = ref('') const searchFilter = ref('')
const selectAll = ref(false) const selectAll = ref(false)
const selectedProjectType = ref('All') const selectedProjectType = ref('All')
@@ -576,7 +592,7 @@ const updateAll = async () => {
projects.value[project].updating = false projects.value[project].updating = false
} }
mixpanel.track('InstanceUpdateAll', { mixpanel_track('InstanceUpdateAll', {
loader: props.instance.metadata.loader, loader: props.instance.metadata.loader,
game_version: props.instance.metadata.game_version, game_version: props.instance.metadata.game_version,
count: setProjects.length, count: setProjects.length,
@@ -601,7 +617,7 @@ const updateProject = async (mod) => {
mod.version = mod.updateVersion.version_number mod.version = mod.updateVersion.version_number
mod.updateVersion = null mod.updateVersion = null
mixpanel.track('InstanceProjectUpdate', { mixpanel_track('InstanceProjectUpdate', {
loader: props.instance.metadata.loader, loader: props.instance.metadata.loader,
game_version: props.instance.metadata.game_version, game_version: props.instance.metadata.game_version,
id: mod.id, id: mod.id,
@@ -628,7 +644,7 @@ const toggleDisableMod = async (mod) => {
.then((newPath) => { .then((newPath) => {
mod.path = newPath mod.path = newPath
mod.disabled = !mod.disabled mod.disabled = !mod.disabled
mixpanel.track('InstanceProjectDisable', { mixpanel_track('InstanceProjectDisable', {
loader: props.instance.metadata.loader, loader: props.instance.metadata.loader,
game_version: props.instance.metadata.game_version, game_version: props.instance.metadata.game_version,
id: mod.id, id: mod.id,
@@ -649,7 +665,7 @@ const removeMod = async (mod) => {
await remove_project(props.instance.path, mod.path).catch(handleError) await remove_project(props.instance.path, mod.path).catch(handleError)
projects.value = projects.value.filter((x) => mod.path !== x.path) projects.value = projects.value.filter((x) => mod.path !== x.path)
mixpanel.track('InstanceProjectRemove', { mixpanel_track('InstanceProjectRemove', {
loader: props.instance.metadata.loader, loader: props.instance.metadata.loader,
game_version: props.instance.metadata.game_version, game_version: props.instance.metadata.game_version,
id: mod.id, id: mod.id,
@@ -778,7 +794,7 @@ listen('tauri://file-drop', async (event) => {
} }
initProjects(await get(props.instance.path).catch(handleError)) initProjects(await get(props.instance.path).catch(handleError))
} }
mixpanel.track('InstanceCreate', { mixpanel_track('InstanceCreate', {
source: 'FileDrop', source: 'FileDrop',
}) })
}) })

View File

@@ -90,7 +90,12 @@
Allows you to change the mod loader, loader version, or game version of the instance. Allows you to change the mod loader, loader version, or game version of the instance.
</span> </span>
</label> </label>
<button id="edit-versions" class="btn" @click="$refs.changeVersionsModal.show()"> <button
id="edit-versions"
class="btn"
@click="$refs.changeVersionsModal.show()"
:disabled="offline"
>
<EditIcon /> <EditIcon />
Edit versions Edit versions
</button> </button>
@@ -291,7 +296,7 @@
<button <button
id="repair-profile" id="repair-profile"
class="btn btn-highlight" class="btn btn-highlight"
:disabled="repairing" :disabled="repairing || offline"
@click="repairProfile" @click="repairProfile"
> >
<HammerIcon /> Repair <HammerIcon /> Repair
@@ -308,7 +313,7 @@
<button <button
id="repair-profile" id="repair-profile"
class="btn btn-highlight" class="btn btn-highlight"
:disabled="repairing" :disabled="repairing || offline"
@click="repairModpack" @click="repairModpack"
> >
<DownloadIcon /> Reinstall <DownloadIcon /> Reinstall
@@ -373,7 +378,7 @@ import { open } from '@tauri-apps/api/dialog'
import { get_fabric_versions, get_forge_versions, get_quilt_versions } from '@/helpers/metadata.js' import { get_fabric_versions, get_forge_versions, get_quilt_versions } from '@/helpers/metadata.js'
import { get_game_versions, get_loaders } from '@/helpers/tags.js' import { get_game_versions, get_loaders } from '@/helpers/tags.js'
import { handleError } from '@/store/notifications.js' import { handleError } from '@/store/notifications.js'
import mixpanel from 'mixpanel-browser' import { mixpanel_track } from '@/helpers/mixpanel'
import { useTheming } from '@/store/theme.js' import { useTheming } from '@/store/theme.js'
const router = useRouter() const router = useRouter()
@@ -383,6 +388,10 @@ const props = defineProps({
type: Object, type: Object,
required: true, required: true,
}, },
offline: {
type: Boolean,
default: false,
},
}) })
const themeStore = useTheming() const themeStore = useTheming()
@@ -403,7 +412,7 @@ const availableGroups = ref([
async function resetIcon() { async function resetIcon() {
icon.value = null icon.value = null
await edit_icon(props.instance.path, null).catch(handleError) await edit_icon(props.instance.path, null).catch(handleError)
mixpanel.track('InstanceRemoveIcon') mixpanel_track('InstanceRemoveIcon')
} }
async function setIcon() { async function setIcon() {
@@ -422,7 +431,7 @@ async function setIcon() {
icon.value = value icon.value = value
await edit_icon(props.instance.path, icon.value).catch(handleError) await edit_icon(props.instance.path, icon.value).catch(handleError)
mixpanel.track('InstanceSetIcon') mixpanel_track('InstanceSetIcon')
} }
const globalSettings = await get().catch(handleError) const globalSettings = await get().catch(handleError)
@@ -536,7 +545,7 @@ async function repairProfile() {
await install(props.instance.path).catch(handleError) await install(props.instance.path).catch(handleError)
repairing.value = false repairing.value = false
mixpanel.track('InstanceRepair', { mixpanel_track('InstanceRepair', {
loader: props.instance.metadata.loader, loader: props.instance.metadata.loader,
game_version: props.instance.metadata.game_version, game_version: props.instance.metadata.game_version,
}) })
@@ -547,7 +556,7 @@ async function repairModpack() {
await update_repair_modrinth(props.instance.path).catch(handleError) await update_repair_modrinth(props.instance.path).catch(handleError)
repairing.value = false repairing.value = false
mixpanel.track('InstanceRepair', { mixpanel_track('InstanceRepair', {
loader: props.instance.metadata.loader, loader: props.instance.metadata.loader,
game_version: props.instance.metadata.game_version, game_version: props.instance.metadata.game_version,
}) })
@@ -559,7 +568,7 @@ async function removeProfile() {
await remove(props.instance.path).catch(handleError) await remove(props.instance.path).catch(handleError)
removing.value = false removing.value = false
mixpanel.track('InstanceRemove', { mixpanel_track('InstanceRemove', {
loader: props.instance.metadata.loader, loader: props.instance.metadata.loader,
game_version: props.instance.metadata.game_version, game_version: props.instance.metadata.game_version,
}) })

View File

@@ -93,7 +93,7 @@ import {
Button, Button,
} from 'omorphia' } from 'omorphia'
import { ref } from 'vue' import { ref } from 'vue'
import mixpanel from 'mixpanel-browser' import { mixpanel_track } from '@/helpers/mixpanel'
const props = defineProps({ const props = defineProps({
project: { project: {
@@ -112,7 +112,7 @@ const nextImage = () => {
expandedGalleryIndex.value = 0 expandedGalleryIndex.value = 0
} }
expandedGalleryItem.value = props.project.gallery[expandedGalleryIndex.value] expandedGalleryItem.value = props.project.gallery[expandedGalleryIndex.value]
mixpanel.track('GalleryImageNext', { mixpanel_track('GalleryImageNext', {
project_id: props.project.id, project_id: props.project.id,
url: expandedGalleryItem.value.url, url: expandedGalleryItem.value.url,
}) })
@@ -124,7 +124,7 @@ const previousImage = () => {
expandedGalleryIndex.value = props.project.gallery.length - 1 expandedGalleryIndex.value = props.project.gallery.length - 1
} }
expandedGalleryItem.value = props.project.gallery[expandedGalleryIndex.value] expandedGalleryItem.value = props.project.gallery[expandedGalleryIndex.value]
mixpanel.track('GalleryImagePrevious', { mixpanel_track('GalleryImagePrevious', {
project_id: props.project.id, project_id: props.project.id,
url: expandedGalleryItem.value, url: expandedGalleryItem.value,
}) })
@@ -135,7 +135,7 @@ const expandImage = (item, index) => {
expandedGalleryIndex.value = index expandedGalleryIndex.value = index
zoomedIn.value = false zoomedIn.value = false
mixpanel.track('GalleryImageExpand', { mixpanel_track('GalleryImageExpand', {
project_id: props.project.id, project_id: props.project.id,
url: item.url, url: item.url,
}) })

View File

@@ -267,7 +267,7 @@ import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime' import relativeTime from 'dayjs/plugin/relativeTime'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import { ref, shallowRef, watch } from 'vue' import { ref, shallowRef, watch } from 'vue'
import { installVersionDependencies } from '@/helpers/utils' import { installVersionDependencies, isOffline } from '@/helpers/utils'
import InstallConfirmModal from '@/components/ui/InstallConfirmModal.vue' import InstallConfirmModal from '@/components/ui/InstallConfirmModal.vue'
import ModInstallModal from '@/components/ui/ModInstallModal.vue' import ModInstallModal from '@/components/ui/ModInstallModal.vue'
import { useBreadcrumbs } from '@/store/breadcrumbs' import { useBreadcrumbs } from '@/store/breadcrumbs'
@@ -276,7 +276,7 @@ import { useFetch } from '@/helpers/fetch.js'
import { handleError } from '@/store/notifications.js' import { handleError } from '@/store/notifications.js'
import { convertFileSrc } from '@tauri-apps/api/tauri' import { convertFileSrc } from '@tauri-apps/api/tauri'
import ContextMenu from '@/components/ui/ContextMenu.vue' import ContextMenu from '@/components/ui/ContextMenu.vue'
import mixpanel from 'mixpanel-browser' import { mixpanel_track } from '@/helpers/mixpanel'
const route = useRoute() const route = useRoute()
const breadcrumbs = useBreadcrumbs() const breadcrumbs = useBreadcrumbs()
@@ -297,6 +297,8 @@ const instance = ref(null)
const installed = ref(false) const installed = ref(false)
const installedVersion = ref(null) const installedVersion = ref(null)
const offline = ref(await isOffline())
async function fetchProjectData() { async function fetchProjectData() {
;[ ;[
data.value, data.value,
@@ -325,7 +327,7 @@ async function fetchProjectData() {
: null : null
} }
await fetchProjectData() if (!offline.value) await fetchProjectData()
watch( watch(
() => route.params.id, () => route.params.id,
@@ -392,7 +394,7 @@ async function install(version) {
data.value.icon_url data.value.icon_url
).catch(handleError) ).catch(handleError)
mixpanel.track('PackInstall', { mixpanel_track('PackInstall', {
id: data.value.id, id: data.value.id,
version_id: queuedVersionData.id, version_id: queuedVersionData.id,
title: data.value.title, title: data.value.title,
@@ -434,7 +436,7 @@ async function install(version) {
await installMod(instance.value.path, selectedVersion.id).catch(handleError) await installMod(instance.value.path, selectedVersion.id).catch(handleError)
await installVersionDependencies(instance.value, queuedVersionData) await installVersionDependencies(instance.value, queuedVersionData)
installedVersion.value = selectedVersion.id installedVersion.value = selectedVersion.id
mixpanel.track('ProjectInstall', { mixpanel_track('ProjectInstall', {
loader: instance.value.metadata.loader, loader: instance.value.metadata.loader,
game_version: instance.value.metadata.game_version, game_version: instance.value.metadata.game_version,
id: data.value.id, id: data.value.id,
@@ -458,7 +460,7 @@ async function install(version) {
await installMod(instance.value.path, queuedVersionData.id).catch(handleError) await installMod(instance.value.path, queuedVersionData.id).catch(handleError)
await installVersionDependencies(instance.value, queuedVersionData) await installVersionDependencies(instance.value, queuedVersionData)
installedVersion.value = queuedVersionData.id installedVersion.value = queuedVersionData.id
mixpanel.track('ProjectInstall', { mixpanel_track('ProjectInstall', {
loader: instance.value.metadata.loader, loader: instance.value.metadata.loader,
game_version: instance.value.metadata.game_version, game_version: instance.value.metadata.game_version,
id: data.value.id, id: data.value.id,

View File

@@ -23,3 +23,7 @@ export const handleError = (err) => {
}) })
console.error(err) console.error(err)
} }
export const handleMixpanelError = (err) => {
console.error(err)
}