diff --git a/src/app/browser/index.ts b/src/app/browser/index.ts
index 34bfc2e..189cec4 100644
--- a/src/app/browser/index.ts
+++ b/src/app/browser/index.ts
@@ -4,11 +4,13 @@ import App from './main/index.js';
import Friend from './friend/index.js';
import DiscordSetup from './discord/index.js';
import AddFriend from './add-friend/index.js';
+import Preferences from './preferences/index.js';
AppRegistry.registerComponent('App', () => App);
AppRegistry.registerComponent('Friend', () => Friend);
AppRegistry.registerComponent('DiscordPresence', () => DiscordSetup);
AppRegistry.registerComponent('AddFriend', () => AddFriend);
+AppRegistry.registerComponent('Preferences', () => Preferences);
const style = window.document.createElement('style');
diff --git a/src/app/browser/main/discord.tsx b/src/app/browser/main/discord.tsx
index a71c87a..342e445 100644
--- a/src/app/browser/main/discord.tsx
+++ b/src/app/browser/main/discord.tsx
@@ -4,7 +4,7 @@ import { User } from 'discord-rpc';
import ipc, { events } from '../ipc.js';
import { RequestState, useAsync, useEventListener } from '../util.js';
import { DiscordPresenceSource, DiscordPresenceSourceUrl, DiscordPresenceSourceCoral } from '../../common/types.js';
-import { DiscordPresence } from '../../../discord/util.js';
+import { DiscordPresence } from '../../../discord/types.js';
import { DISCORD_COLOUR, TEXT_COLOUR_DARK } from '../constants.js';
export default function DiscordPresenceSource(props: {
diff --git a/src/app/browser/preferences/index.tsx b/src/app/browser/preferences/index.tsx
new file mode 100644
index 0000000..72ae840
--- /dev/null
+++ b/src/app/browser/preferences/index.tsx
@@ -0,0 +1,353 @@
+import React, { useCallback, useEffect, useMemo, useState } from 'react';
+import { StyleSheet, Text, TextInput, TouchableHighlight, TouchableOpacity, useColorScheme, View } from 'react-native';
+import { CheckBox, Picker } from 'react-native-web';
+import { DiscordPresencePlayTime } from '../../../discord/types.js';
+import { Button } from '../components/index.js';
+import { DEFAULT_ACCENT_COLOUR, HIGHLIGHT_COLOUR_DARK, HIGHLIGHT_COLOUR_LIGHT, TEXT_COLOUR_DARK, TEXT_COLOUR_LIGHT } from '../constants.js';
+import ipc, { events } from '../ipc.js';
+import { getAccounts, RequestState, Root, useAsync, useDiscordPresenceSource, useEventListener } from '../util.js';
+
+export interface PreferencesProps {}
+
+export default function Preferences(props: PreferencesProps) {
+ const colour_scheme = useColorScheme();
+ const theme = colour_scheme === 'light' ? light : dark;
+
+ const [accent_colour, setAccentColour] = React.useState(() => ipc.getAccentColour());
+ useEventListener(events, 'systemPreferences:accent-colour', setAccentColour, []);
+
+ const [users, ,, forceRefreshAccounts] = useAsync(useCallback(() => getAccounts(), [ipc]));
+ useEventListener(events, 'update-nintendo-accounts', forceRefreshAccounts, []);
+
+ const [login_item, ,, forceRefreshLoginItem] = useAsync(useCallback(() => ipc.getLoginItemSettings(), [ipc]));
+
+ const setOpenAtLogin = useCallback(async (open_at_login: boolean | 'mixed') => {
+ await ipc.setLoginItemSettings({...login_item, openAtLogin: !!open_at_login});
+ forceRefreshLoginItem();
+ }, [ipc, login_item]);
+ const setOpenAsHidden = useCallback(async (open_as_hidden: boolean | 'mixed') => {
+ await ipc.setLoginItemSettings({...login_item, openAsHidden: !!open_as_hidden});
+ forceRefreshLoginItem();
+ }, [ipc, login_item]);
+
+ const [discord_users, discord_users_error, discord_users_state, forceRefreshDiscordUsers] =
+ useAsync(useCallback(() => ipc.getDiscordUsers(), [ipc]));
+
+ const [discord_options, , discord_options_state, forceRefreshDiscordOptions] =
+ useAsync(useCallback(() => ipc.getSavedDiscordPresenceOptions(), [ipc]));
+
+ const [discord_friend_code, setDiscordFriendCodeValue] = useState(discord_options?.friend_code ?? '');
+ const discord_friend_code_valid = !discord_friend_code || discord_friend_code.match(/^\d{4}-\d{4}-\d{4}$/);
+
+ useEffect(() => {
+ setDiscordFriendCodeValue(discord_options?.friend_code ?? '');
+ }, [discord_options]);
+
+ const setDiscordUser = useCallback(async (user: string | undefined) => {
+ if (user === '*') user = undefined;
+ await ipc.setDiscordPresenceOptions({...discord_options, user});
+ forceRefreshDiscordOptions();
+ }, [ipc, discord_options]);
+ const setDiscordFriendCode = useCallback(async (friend_code: string | undefined) => {
+ setDiscordFriendCodeValue(friend_code ?? '');
+ if (friend_code && !friend_code.match(/^\d{4}-\d{4}-\d{4}$/)) return;
+ if (!friend_code) friend_code = undefined;
+ await ipc.setDiscordPresenceOptions({...discord_options, friend_code});
+ forceRefreshDiscordOptions();
+ }, [ipc, discord_options]);
+ const setDiscordShowConsoleOnline = useCallback(async (show_console_online: boolean | 'mixed') => {
+ await ipc.setDiscordPresenceOptions({...discord_options, show_console_online: !!show_console_online});
+ forceRefreshDiscordOptions();
+ }, [ipc, discord_options]);
+ const setDiscordShowPlayTime = useCallback(async (show_play_time: DiscordPresencePlayTime) => {
+ await ipc.setDiscordPresenceOptions({...discord_options, show_play_time});
+ forceRefreshDiscordOptions();
+ }, [ipc, discord_options]);
+
+ const [discord_presence_source, discord_presence_source_state] = useDiscordPresenceSource();
+
+ const discord_presence_source_user = discord_presence_source && 'na_id' in discord_presence_source ?
+ discord_presence_source.friend_nsa_id ?
+ users?.find(u => u.nso?.nsoAccount.user.nsaId === discord_presence_source.friend_nsa_id)?.nso :
+ users?.find(u => u.nso?.user.id === discord_presence_source.na_id)?.nso :
+ null;
+
+ const discord_friend_code_self = discord_presence_source_user?.nsoAccount.user.links.friendCode.id;
+ const [is_discord_friend_code_self, setIsDiscordFriendCodeSelf] = useState(false);
+ useEffect(() => {
+ setIsDiscordFriendCodeSelf(!!discord_presence_source && (!discord_friend_code || discord_friend_code === discord_friend_code_self));
+ }, [discord_presence_source, discord_friend_code_self]);
+
+ useEventListener(events, 'window:refresh', () => (
+ forceRefreshAccounts(), forceRefreshLoginItem(),
+ forceRefreshDiscordUsers(), forceRefreshDiscordOptions()
+ ), []);
+
+ if (!users ||
+ !login_item ||
+ !discord_options ||
+ discord_presence_source_state !== RequestState.LOADED
+ ) {
+ return null;
+ }
+
+ const discord_user_picker = [];
+
+ if (discord_options?.user && !discord_users?.find(u => u.id === discord_options.user)) {
+ discord_user_picker.push();
+ }
+ for (const user of discord_users ?? []) {
+ discord_user_picker.push();
+ }
+
+ return
+
+ {/* Preferences */}
+
+ {ipc.platform === 'darwin' || ipc.platform === 'win32' ?
+
+ Startup
+
+
+ {/* Launch at startup menu here
+ {JSON.stringify(login_item, null, 4)} */}
+
+
+
+ setOpenAtLogin(!login_item.openAtLogin)}>
+ Open at login
+
+
+
+ {ipc.platform === 'darwin' ?
+
+ setOpenAsHidden(!login_item.openAsHidden)}>
+ Open in background
+
+ : null}
+
+ : null}
+
+
+
+ Sleep
+
+
+ Prevent sleep menu here
+
+
+
+
+
+ Discord Rich Presence
+
+
+ Discord Rich Presence is {discord_presence_source ? 'en' : 'dis'}abled.
+
+
+
+
+ Discord user
+
+ selectedValue={discord_options?.user ?? '*'} onValueChange={setDiscordUser}
+ style={[styles.picker, theme.picker]}
+ enabled={discord_options_state !== RequestState.LOADING &&
+ discord_users_state !== RequestState.LOADING}
+ >{...discord_user_picker}
+
+ Friend code
+ Adding your friend code will also show your Nintendo Switch user icon in Discord.
+
+ {is_discord_friend_code_self ? <>
+
+ setDiscordFriendCode(v ? discord_friend_code_self : undefined)}
+ color={'#' + (accent_colour ?? DEFAULT_ACCENT_COLOUR)}
+ style={styles.checkbox}
+ />
+ setDiscordFriendCode(discord_options.friend_code ? undefined : discord_friend_code_self)}>
+ Share my friend code
+
+
+
+ setIsDiscordFriendCodeSelf(false)}>
+
+ Set custom friend code
+
+
+ > : }
+
+ {/* */}
+
+
+
+ setDiscordShowConsoleOnline(!discord_options?.show_console_online)}>
+ Show "Not playing" when a console linked to your account is online, but you are not selected in a game
+
+
+
+ Play time
+
+
+ selectedValue={'' + (discord_options?.show_play_time ??
+ DiscordPresencePlayTime.DETAILED_PLAY_TIME_SINCE)}
+ onValueChange={v => setDiscordShowPlayTime(parseInt(v))}
+ style={[styles.picker, theme.picker]}
+ enabled={discord_options_state !== RequestState.LOADING}
+ >
+
+
+
+
+
+
+
+
+
+
+ ;
+}
+
+const styles = StyleSheet.create({
+ loading: {
+ flex: 1,
+ paddingVertical: 50,
+ paddingHorizontal: 20,
+ justifyContent: 'center',
+ },
+
+ main: {
+ flex: 1,
+ paddingVertical: 20,
+ paddingHorizontal: 35,
+ },
+
+ section: {
+ marginBottom: 15,
+ flexDirection: 'row',
+ },
+ sectionLeft: {
+ // flex: 0,
+ width: '30%',
+ marginRight: 30,
+ },
+ label: {
+ textAlign: 'right',
+ },
+ sectionRight: {
+ flex: 1,
+ },
+
+ header: {
+ marginTop: 12,
+ },
+ help: {
+ marginTop: 8,
+ fontSize: 13,
+ opacity: 0.7,
+ },
+
+ textLinkTouchable: {
+ marginBottom: 8,
+ },
+ textLink: {
+ fontSize: 13,
+ opacity: 0.85,
+ },
+
+ picker: {
+ marginTop: 8,
+ paddingVertical: 4,
+ paddingHorizontal: 8,
+ borderWidth: 0,
+ borderRadius: 3,
+ fontSize: 13,
+ },
+ textInput: {
+ marginTop: 8,
+ paddingVertical: 5,
+ paddingHorizontal: 10,
+ borderRadius: 3,
+ fontSize: 13,
+ },
+ checkboxContainer: {
+ flex: 1,
+ flexBasis: 'auto',
+ // marginRight: 20,
+ flexDirection: 'row',
+ alignItems: 'center',
+ // maxWidth: 1,
+ },
+ checkbox: {
+ marginRight: 10,
+ marginVertical: 8,
+ },
+ checkboxLabel: {
+ flex: 1,
+ marginVertical: 8,
+ },
+
+ button: {
+ marginTop: 10,
+ marginRight: 10,
+ flexDirection: 'row',
+ },
+});
+
+const light = StyleSheet.create({
+ text: {
+ color: TEXT_COLOUR_LIGHT,
+ },
+ picker: {
+ backgroundColor: HIGHLIGHT_COLOUR_LIGHT,
+ color: TEXT_COLOUR_LIGHT,
+ },
+ textInput: {
+ backgroundColor: HIGHLIGHT_COLOUR_LIGHT,
+ color: TEXT_COLOUR_LIGHT,
+ },
+});
+
+const dark = StyleSheet.create({
+ text: {
+ color: TEXT_COLOUR_DARK,
+ },
+ picker: {
+ backgroundColor: HIGHLIGHT_COLOUR_DARK,
+ color: TEXT_COLOUR_DARK,
+ },
+ textInput: {
+ backgroundColor: HIGHLIGHT_COLOUR_DARK,
+ color: TEXT_COLOUR_DARK,
+ },
+});
diff --git a/src/app/browser/util.tsx b/src/app/browser/util.tsx
index bef9088..1d3df01 100644
--- a/src/app/browser/util.tsx
+++ b/src/app/browser/util.tsx
@@ -3,7 +3,7 @@ import React, { useCallback, useEffect, useLayoutEffect, useState } from 'react'
import { ColorSchemeName, LayoutChangeEvent, Platform, StyleProp, StyleSheet, useColorScheme, View, ViewStyle } from 'react-native';
import type { User as DiscordUser } from 'discord-rpc';
import { ErrorResponse } from '../../api/util.js';
-import { DiscordPresence } from '../../discord/util.js';
+import { DiscordPresence } from '../../discord/types.js';
import ipc, { events } from './ipc.js';
import { NintendoAccountUser } from '../../api/na.js';
import { SavedToken } from '../../common/auth/coral.js';
diff --git a/src/app/common/types.ts b/src/app/common/types.ts
index 59b107e..bbc6972 100644
--- a/src/app/common/types.ts
+++ b/src/app/common/types.ts
@@ -1,10 +1,11 @@
-import { DiscordPresencePlayTime } from '../../discord/util.js';
+import { DiscordPresencePlayTime } from '../../discord/types.js';
export enum WindowType {
MAIN_WINDOW = 'App',
FRIEND = 'Friend',
DISCORD_PRESENCE = 'DiscordPresence',
ADD_FRIEND = 'AddFriend',
+ PREFERENCES = 'Preferences',
}
interface WindowProps {
@@ -12,6 +13,7 @@ interface WindowProps {
[WindowType.FRIEND]: import('../browser/friend/index.js').FriendProps;
[WindowType.DISCORD_PRESENCE]: import('../browser/discord/index.js').DiscordSetupProps;
[WindowType.ADD_FRIEND]: import('../browser/add-friend/index.js').AddFriendProps;
+ [WindowType.PREFERENCES]: import('../browser/preferences/index.js').PreferencesProps;
}
export interface WindowConfiguration {
diff --git a/src/app/main/app-menu.ts b/src/app/main/app-menu.ts
index dfcb305..fc34b4b 100644
--- a/src/app/main/app-menu.ts
+++ b/src/app/main/app-menu.ts
@@ -1,7 +1,36 @@
import { GITHUB_MIRROR_URL, GITLAB_URL, ISSUES_URL } from '../../common/constants.js';
import { BrowserWindow, Menu, MenuItem, shell } from './electron.js';
+import { App } from './index.js';
+
+let appinstance: App | null;
+
+export function setAppInstance(app: App) {
+ appinstance = app;
+}
+
+const menu_app = new MenuItem({
+ role: 'appMenu',
+ submenu: [
+ { role: 'about' },
+ { type: 'separator' },
+ {
+ label: 'Preferences',
+ accelerator: 'CommandOrControl+,',
+ click: () => {
+ appinstance?.showPreferencesWindow();
+ },
+ },
+ { type: 'separator' },
+ { role: 'services' },
+ { type: 'separator' },
+ { role: 'hide' },
+ { role: 'hideOthers' },
+ { role: 'unhide' },
+ { type: 'separator' },
+ { role: 'quit' },
+ ],
+});
-const menu_app = new MenuItem({role: 'appMenu'});
const menu_file = new MenuItem({role: 'fileMenu'});
const menu_edit = new MenuItem({role: 'editMenu'});
diff --git a/src/app/main/electron.ts b/src/app/main/electron.ts
index b2edfe3..f4b229a 100644
--- a/src/app/main/electron.ts
+++ b/src/app/main/electron.ts
@@ -23,10 +23,13 @@ export type BrowserWindow = import('electron').BrowserWindow;
export type BrowserWindowConstructorOptions = import('electron').BrowserWindowConstructorOptions;
export type IpcMain = import('electron').IpcMain;
export type IpcMainInvokeEvent = import('electron').IpcMainInvokeEvent;
+export type LoginItemSettings = import('electron').LoginItemSettings;
+export type LoginItemSettingsOptions = import('electron').LoginItemSettingsOptions;
export type Menu = import('electron').Menu;
export type MenuItem = import('electron').MenuItem;
export type MessageBoxOptions = import('electron').MessageBoxOptions;
export type Notification = import('electron').Notification;
+export type Settings = import('electron').Settings;
export type ShareMenu = import('electron').ShareMenu;
export type SharingItem = import('electron').SharingItem;
export type Tray = import('electron').Tray;
diff --git a/src/app/main/index.ts b/src/app/main/index.ts
index 245c742..0df8652 100644
--- a/src/app/main/index.ts
+++ b/src/app/main/index.ts
@@ -1,4 +1,4 @@
-import { app, BrowserWindow, dialog, ipcMain } from './electron.js';
+import { app, BrowserWindow, dialog, ipcMain, LoginItemSettingsOptions } from './electron.js';
import process from 'node:process';
import * as path from 'node:path';
import { EventEmitter } from 'node:events';
@@ -18,9 +18,12 @@ import { setupIpc } from './ipc.js';
import { dev, dir } from '../../util/product.js';
import { addUserAgent } from '../../util/useragent.js';
import { askUserForUri } from './util.js';
+import { setAppInstance } from './app-menu.js';
const debug = createDebug('app:main');
+export const login_item_options: LoginItemSettingsOptions = {};
+
export class App {
readonly store: Store;
readonly monitors: PresenceMonitorManager;
@@ -58,6 +61,32 @@ export class App {
return this.main_window = window;
}
+
+ preferences_window: BrowserWindow | null = null;
+
+ showPreferencesWindow() {
+ if (this.preferences_window) {
+ this.preferences_window.show();
+ this.preferences_window.focus();
+ return this.preferences_window;
+ }
+
+ const window = createWindow(WindowType.PREFERENCES, {}, {
+ show: false,
+ maximizable: false,
+ minimizable: false,
+ width: 580,
+ height: 400,
+ minWidth: 580,
+ maxWidth: 580,
+ minHeight: 400,
+ maxHeight: 400,
+ });
+
+ window.on('closed', () => this.preferences_window = null);
+
+ return this.preferences_window = window;
+ }
}
export async function init() {
@@ -82,11 +111,12 @@ export async function init() {
const storage = await initStorage(process.env.NXAPI_DATA_PATH ?? paths.data);
const appinstance = new App(storage);
- setupIpc(appinstance, ipcMain);
-
// @ts-expect-error
globalThis.app = appinstance;
+ setAppInstance(appinstance);
+ setupIpc(appinstance, ipcMain);
+
appinstance.store.restoreMonitorState(appinstance.monitors);
const menu = new MenuApp(appinstance);
@@ -136,7 +166,9 @@ export async function init() {
debug('App started');
- appinstance.showMainWindow();
+ if (!app.getLoginItemSettings(login_item_options).wasOpenedAsHidden) {
+ appinstance.showMainWindow();
+ }
}
function tryHandleUrl(app: App, url: string) {
@@ -243,6 +275,20 @@ export class Store extends EventEmitter {
debug('Saving monitor state', state);
await this.storage.setItem('AppMonitors', state);
+
+ if (state.discord_presence) {
+ await this.storage.setItem('AppDiscordPresenceOptions', {
+ ...state.discord_presence,
+ source: undefined,
+ });
+ }
+ }
+
+ async getSavedDiscordPresenceOptions() {
+ const options: Omit | undefined =
+ await this.storage.getItem('AppDiscordPresenceOptions');
+
+ return options ?? null;
}
async restoreMonitorState(monitors: PresenceMonitorManager) {
diff --git a/src/app/main/ipc.ts b/src/app/main/ipc.ts
index 0946ccd..b33f49c 100644
--- a/src/app/main/ipc.ts
+++ b/src/app/main/ipc.ts
@@ -1,4 +1,4 @@
-import { BrowserWindow, clipboard, dialog, IpcMain, Menu, MenuItem, ShareMenu, SharingItem, shell, systemPreferences } from './electron.js';
+import { app, BrowserWindow, clipboard, dialog, IpcMain, LoginItemSettings, Menu, MenuItem, Settings, ShareMenu, SharingItem, shell, systemPreferences } from './electron.js';
import * as util from 'node:util';
import createDebug from 'debug';
import { User } from 'discord-rpc';
@@ -7,10 +7,10 @@ import { createWindow, getWindowConfiguration } from './windows.js';
import { DiscordPresenceConfiguration, DiscordPresenceSource, WindowType } from '../common/types.js';
import { CurrentUser, Friend, Game, PresenceState, WebService } from '../../api/coral-types.js';
import { askAddNsoAccount, askAddPctlAccount } from './na-auth.js';
-import { App } from './index.js';
+import { App, login_item_options } from './index.js';
import { NintendoAccountUser } from '../../api/na.js';
import { hrduration } from '../../util/misc.js';
-import { DiscordPresence } from '../../discord/util.js';
+import { DiscordPresence } from '../../discord/types.js';
import { getDiscordRpcClients } from '../../discord/rpc.js';
import { defaultTitle } from '../../discord/titles.js';
import type { FriendProps } from '../browser/friend/index.js';
@@ -20,6 +20,8 @@ import { AddFriendProps } from '../browser/add-friend/index.js';
const debug = createDebug('app:main:ipc');
+const shown_modal_windows = new WeakSet();
+
export function setupIpc(appinstance: App, ipcMain: IpcMain) {
const store = appinstance.store;
const storage = appinstance.store.storage;
@@ -38,6 +40,9 @@ export function setupIpc(appinstance: App, ipcMain: IpcMain) {
sendToAllWindows('nxapi:systemPreferences:accent-colour', accent_colour);
});
+ ipcMain.handle('nxapi:systemPreferences:getloginitem', () => app.getLoginItemSettings(login_item_options));
+ ipcMain.handle('nxapi:systemPreferences:setloginitem', (e, settings: Settings) => app.setLoginItemSettings({...login_item_options, ...settings}));
+
ipcMain.handle('nxapi:update:get', () => appinstance.updater.cache ?? appinstance.updater.check());
ipcMain.handle('nxapi:update:check', () => appinstance.updater.check());
@@ -128,11 +133,18 @@ export function setupIpc(appinstance: App, ipcMain: IpcMain) {
window.setMaximumSize(maxWidth, height + (curHeight - curContentHeight));
}
window.setContentSize(curContentWidth, height);
- window.show();
+
+ if (!shown_modal_windows.has(window)) {
+ window.show();
+ shown_modal_windows.add(window);
+ }
});
ipcMain.handle('nxapi:discord:config', () => appinstance.monitors.getDiscordPresenceConfiguration());
ipcMain.handle('nxapi:discord:setconfig', (e, config: DiscordPresenceConfiguration | null) => appinstance.monitors.setDiscordPresenceConfiguration(config));
+ ipcMain.handle('nxapi:discord:options', () => appinstance.monitors.getActiveDiscordPresenceOptions() ?? appinstance.store.getSavedDiscordPresenceOptions());
+ ipcMain.handle('nxapi:discord:savedoptions', () => appinstance.store.getSavedDiscordPresenceOptions());
+ ipcMain.handle('nxapi:discord:setoptions', (e, options: Omit) => appinstance.monitors.setDiscordPresenceOptions(options));
ipcMain.handle('nxapi:discord:source', () => appinstance.monitors.getDiscordPresenceSource());
ipcMain.handle('nxapi:discord:setsource', (e, source: DiscordPresenceSource | null) => appinstance.monitors.setDiscordPresenceSource(source));
ipcMain.handle('nxapi:discord:presence', () => appinstance.monitors.getDiscordPresence());
diff --git a/src/app/main/menu.ts b/src/app/main/menu.ts
index a789b1e..d2551b2 100644
--- a/src/app/main/menu.ts
+++ b/src/app/main/menu.ts
@@ -103,6 +103,7 @@ export default class MenuApp {
menu.append(new MenuItem({type: 'separator'}));
menu.append(new MenuItem({label: 'Show main window', click: () => this.app.showMainWindow()}));
+ menu.append(new MenuItem({label: 'Preferences', click: () => this.app.showPreferencesWindow()}));
if (dev) menu.append(new MenuItem({label: 'Dump notifications state', click: () => {
debug('Accounts', this.app.monitors.notifications.accounts);
debug('Friends', this.app.monitors.notifications.onlinefriends);
diff --git a/src/app/main/monitor.ts b/src/app/main/monitor.ts
index c0b4137..26bec96 100644
--- a/src/app/main/monitor.ts
+++ b/src/app/main/monitor.ts
@@ -8,7 +8,7 @@ import { LoopResult } from '../../util/loop.js';
import { tryGetNativeImageFromUrl } from './util.js';
import { App } from './index.js';
import { DiscordPresenceConfiguration, DiscordPresenceSource } from '../common/types.js';
-import { DiscordPresence, DiscordPresencePlayTime } from '../../discord/util.js';
+import { DiscordPresence, DiscordPresencePlayTime } from '../../discord/types.js';
import { DiscordRpcClient } from '../../discord/rpc.js';
const debug = createDebug('app:main:monitor');
@@ -110,14 +110,11 @@ export class PresenceMonitorManager {
return this.getActiveDiscordPresenceMonitor()?.discord.last_activity ?? null;
}
- getDiscordPresenceConfiguration(): DiscordPresenceConfiguration | null {
+ getActiveDiscordPresenceOptions(): Omit | null {
const monitor = this.getActiveDiscordPresenceMonitor();
- const source = this.getDiscordPresenceSource();
-
- if (!monitor || !source) return null;
+ if (!monitor) return null;
return {
- source,
user: this.getDiscordClientFilterConfiguration(monitor.discord_client_filter),
friend_code: monitor.show_friend_code && monitor.force_friend_code ?
monitor.force_friend_code.id : undefined,
@@ -127,6 +124,30 @@ export class PresenceMonitorManager {
};
}
+ async setDiscordPresenceOptions(options: Omit) {
+ const source = this.getDiscordPresenceSource();
+
+ if (!source) {
+ // Discord presence is not active
+ // Save the presence options anyway so they can be restored when Discord presence is enabled
+ return this.app.store.storage.setItem('AppDiscordPresenceOptions', options);
+ }
+
+ await this.setDiscordPresenceSource(source, monitor => {
+ this.setDiscordPresenceConfigurationForMonitor(monitor, options);
+ monitor.skipIntervalInCurrentLoop();
+ });
+
+ await this.app.store.saveMonitorState(this);
+ }
+
+ getDiscordPresenceConfiguration(): DiscordPresenceConfiguration | null {
+ const source = this.getDiscordPresenceSource();
+ const options = this.getActiveDiscordPresenceOptions();
+
+ return source && options ? {source, ...options} : null;
+ }
+
async setDiscordPresenceConfiguration(config: DiscordPresenceConfiguration | null) {
if (!config) return this.setDiscordPresenceSource(null);
@@ -140,7 +161,7 @@ export class PresenceMonitorManager {
setDiscordPresenceConfigurationForMonitor(
monitor: EmbeddedPresenceMonitor | EmbeddedProxyPresenceMonitor,
- config: DiscordPresenceConfiguration
+ config: Omit
) {
monitor.discord_client_filter = config.user ? this.createDiscordClientFilter(config.user) : undefined;
monitor.show_friend_code = !!config.friend_code;
diff --git a/src/app/preload/index.ts b/src/app/preload/index.ts
index 5a84d3a..80dc5e7 100644
--- a/src/app/preload/index.ts
+++ b/src/app/preload/index.ts
@@ -2,17 +2,17 @@ import { contextBridge, ipcRenderer } from 'electron';
import { EventEmitter } from 'events';
import createDebug from 'debug';
import type { User } from 'discord-rpc';
-import type { SharingItem } from '../main/electron.js';
+import type { LoginItemSettings, Settings, SharingItem } from '../main/electron.js';
import type { DiscordPresenceConfiguration, DiscordPresenceSource, WindowConfiguration } from '../common/types.js';
import type { SavedToken } from '../../common/auth/coral.js';
import type { SavedMoonToken } from '../../common/auth/moon.js';
import type { UpdateCacheData } from '../../common/update.js';
import type { Announcements, CoralSuccessResponse, CurrentUser, Friend, FriendCodeUrl, FriendCodeUser, GetActiveEventResult, WebService, WebServices } from '../../api/coral-types.js';
-import type { DiscordPresence } from '../../discord/util.js';
+import type { DiscordPresence } from '../../discord/types.js';
import type { NintendoAccountUser } from '../../api/na.js';
import type { DiscordSetupProps } from '../browser/discord/index.js';
import type { FriendProps } from '../browser/friend/index.js';
-import { AddFriendProps } from '../browser/add-friend/index.js';
+import type { AddFriendProps } from '../browser/add-friend/index.js';
// In sandboxed renderers the process object contains a very limited set of APIs
// https://www.electronjs.org/docs/latest/api/process#sandbox
@@ -30,6 +30,9 @@ events.setMaxListeners(0);
const ipc = {
getWindowData: () => invSync('browser:getwindowdata'),
+ getLoginItemSettings: () => inv('systemPreferences:getloginitem'),
+ setLoginItemSettings: (settings: Settings) => inv('systemPreferences:setloginitem', settings),
+
getUpdateData: () => inv('update:get'),
checkUpdates: () => inv('update:check'),
@@ -50,6 +53,9 @@ const ipc = {
getDiscordPresenceConfig: () => inv('discord:config'),
setDiscordPresenceConfig: (config: DiscordPresenceConfiguration | null) => inv('discord:setconfig', config),
+ getDiscordPresenceOptions: () => inv | null>('discord:options'),
+ getSavedDiscordPresenceOptions: () => inv | null>('discord:savedoptions'),
+ setDiscordPresenceOptions: (options: Omit) => inv('discord:setoptions', options),
getDiscordPresenceSource: () => inv('discord:source'),
setDiscordPresenceSource: (source: DiscordPresenceSource | null) => inv('discord:setsource', source),
getDiscordPresence: () => inv('discord:presence'),
diff --git a/src/cli/nso/presence.ts b/src/cli/nso/presence.ts
index 5a977a1..274214c 100644
--- a/src/cli/nso/presence.ts
+++ b/src/cli/nso/presence.ts
@@ -3,7 +3,7 @@ import type { Arguments as ParentArguments } from '../nso.js';
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
import { initStorage } from '../../util/storage.js';
import { getToken } from '../../common/auth/coral.js';
-import { DiscordPresencePlayTime } from '../../discord/util.js';
+import { DiscordPresencePlayTime } from '../../discord/types.js';
import { handleEnableSplatNet2Monitoring, TerminalNotificationManager } from './notify.js';
import { ZncDiscordPresence, ZncProxyDiscordPresence } from '../../common/presence.js';
diff --git a/src/cli/util/discord-activity.ts b/src/cli/util/discord-activity.ts
index 460a7c0..0d9d3fc 100644
--- a/src/cli/util/discord-activity.ts
+++ b/src/cli/util/discord-activity.ts
@@ -4,7 +4,8 @@ import fetch from 'node-fetch';
import { getPresenceFromUrl } from '../../api/znc-proxy.js';
import { ActiveEvent, CurrentUser, Friend, Game, Presence, PresenceState } from '../../api/coral-types.js';
import type { Arguments as ParentArguments } from '../util.js';
-import { DiscordPresenceContext, DiscordPresencePlayTime, getDiscordPresence, getInactiveDiscordPresence } from '../../discord/util.js';
+import { getDiscordPresence, getInactiveDiscordPresence } from '../../discord/util.js';
+import { DiscordPresenceContext, DiscordPresencePlayTime } from '../../discord/types.js';
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
import { initStorage } from '../../util/storage.js';
import { getToken } from '../../common/auth/coral.js';
diff --git a/src/cli/util/export-discord-titles.ts b/src/cli/util/export-discord-titles.ts
index a34acbf..1520541 100644
--- a/src/cli/util/export-discord-titles.ts
+++ b/src/cli/util/export-discord-titles.ts
@@ -4,7 +4,7 @@ import type { Arguments as ParentArguments } from '../util.js';
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
import { titles as unsorted_titles } from '../../discord/titles.js';
import { DiscordApplicationRpc, getDiscordApplicationRpc } from './discord-activity.js';
-import { Title } from '../../discord/util.js';
+import { Title } from '../../discord/types.js';
const debug = createDebug('cli:util:export-discord-titles');
diff --git a/src/common/presence.ts b/src/common/presence.ts
index 42ed2a6..eeabe5a 100644
--- a/src/common/presence.ts
+++ b/src/common/presence.ts
@@ -1,6 +1,7 @@
import createDebug from 'debug';
import { DiscordRpcClient, findDiscordRpcClient } from '../discord/rpc.js';
-import { DiscordPresencePlayTime, DiscordPresenceContext, getDiscordPresence, getInactiveDiscordPresence, DiscordPresence } from '../discord/util.js';
+import { getDiscordPresence, getInactiveDiscordPresence } from '../discord/util.js';
+import { DiscordPresencePlayTime, DiscordPresenceContext, DiscordPresence } from '../discord/types.js';
import { EmbeddedSplatNet2Monitor, ZncNotifications } from './notify.js';
import { getPresenceFromUrl } from '../api/znc-proxy.js';
import { ActiveEvent, CurrentUser, Friend, Game, Presence, PresenceState, CoralErrorResponse } from '../api/coral-types.js';
diff --git a/src/discord/titles.ts b/src/discord/titles.ts
index 2df75cc..f688b05 100644
--- a/src/discord/titles.ts
+++ b/src/discord/titles.ts
@@ -1,4 +1,4 @@
-import { Title } from './util.js';
+import { Title } from './types.js';
import * as publishers from './titles/index.js';
export const defaultTitle: Title = {
diff --git a/src/discord/titles/capcom.ts b/src/discord/titles/capcom.ts
index 275d702..494787f 100644
--- a/src/discord/titles/capcom.ts
+++ b/src/discord/titles/capcom.ts
@@ -1,4 +1,4 @@
-import { Title } from '../util.js';
+import { Title } from '../types.js';
export const titles: Title[] = [
{
diff --git a/src/discord/titles/mojang.ts b/src/discord/titles/mojang.ts
index 838652d..e71b808 100644
--- a/src/discord/titles/mojang.ts
+++ b/src/discord/titles/mojang.ts
@@ -1,4 +1,4 @@
-import { Title } from '../util.js';
+import { Title } from '../types.js';
export const titles: Title[] = [
{
diff --git a/src/discord/titles/nintendo.ts b/src/discord/titles/nintendo.ts
index b3e844d..6a2d04f 100644
--- a/src/discord/titles/nintendo.ts
+++ b/src/discord/titles/nintendo.ts
@@ -1,4 +1,4 @@
-import { Title } from '../util.js';
+import { Title } from '../types.js';
export const titles: Title[] = [
{
diff --git a/src/discord/titles/the-pokémon-company.ts b/src/discord/titles/the-pokémon-company.ts
index c5126db..25d0514 100644
--- a/src/discord/titles/the-pokémon-company.ts
+++ b/src/discord/titles/the-pokémon-company.ts
@@ -1,4 +1,4 @@
-import { Title } from '../util.js';
+import { Title } from '../types.js';
export const titles: Title[] = [
{
diff --git a/src/discord/types.ts b/src/discord/types.ts
new file mode 100644
index 0000000..e2adfc6
--- /dev/null
+++ b/src/discord/types.ts
@@ -0,0 +1,112 @@
+import DiscordRPC from 'discord-rpc';
+import { ActiveEvent, CurrentUser, Friend, Game } from '../api/coral-types.js';
+import { ZncDiscordPresence, ZncProxyDiscordPresence } from '../common/presence.js';
+
+export interface DiscordPresenceContext {
+ friendcode?: CurrentUser['links']['friendCode'];
+ activeevent?: ActiveEvent;
+ show_play_time?: DiscordPresencePlayTime;
+ znc_discord_presence?: ZncDiscordPresence | ZncProxyDiscordPresence;
+ nsaid?: string;
+ user?: CurrentUser | Friend;
+}
+
+export interface DiscordPresence {
+ id: string;
+ title: string | null;
+ activity: DiscordRPC.Presence;
+ showTimestamp?: boolean;
+}
+
+type SystemModuleTitleId = `01000000000000${string}`;
+type SystemDataTitleId = `01000000000008${string}`;
+type SystemAppletTitleId = `0100000000001${string}`;
+type ApplicationTitleId = `0100${string}${'0' | '2' | '4' | '6' | '8' | 'a' | 'c' | 'e'}000`;
+
+export interface Title {
+ /**
+ * Lowercase hexadecimal title ID.
+ *
+ * Valid application title IDs are 16 characters long, and should start with `0100` and end with `0000`, `2000`, `4000`, `6000`, `8000`, `a000`, `c000`, `e000` (this is because applications have 16^4 title IDs for the application itself, plus addon content and update data).
+ */
+ id: ApplicationTitleId | '0000000000000000';
+ /**
+ * Discord client ID
+ */
+ client: string;
+
+ /**
+ * Title name to show in Discord. This is *not* the name that will appear under the user's name after "Playing ".
+ *
+ * If this is set to true the title's name from znc will be used.
+ * If this is set to false (default) no title name will be set. This should be used when a specific Discord client for the title is used.
+ * If this is set to a string it will be used as the title name.
+ *
+ * @default false
+ */
+ titleName?: string | boolean;
+ /**
+ * By default the title's icon from znc will be used. (No icons need to be uploaded to Discord.)
+ */
+ largeImageKey?: string;
+ largeImageText?: string;
+ /**
+ * By default the user's icon and friend code will be used if the user is sharing their friend code; otherwise it will not be set.
+ */
+ smallImageKey?: string;
+ smallImageText?: string;
+ /**
+ * Whether to show the timestamp the user started playing the title in Discord. Discord shows this as the number of minutes and seconds since the timestamp.
+ *
+ * If enabled this is set to the time the user's presence was last updated as reported by Nintendo. Any changes to the updated timestamp will be ignored as long as the title doesn't change. The timestamp may change if the presence tracking is reset for any reason.
+ *
+ * This is now enabled by default as it's required for the activity to show in the Active Now panel.
+ *
+ * @default true
+ */
+ showTimestamp?: boolean;
+ /**
+ * Show the activity description set by the title.
+ *
+ * @default true
+ */
+ showDescription?: boolean;
+ /**
+ * Show "Playing online" if playing online and the game doesn't set activity details.
+ *
+ * @default false
+ */
+ showPlayingOnline?: string | boolean;
+ /**
+ * Whether to show details of the current event (Online Lounge/voice chat) in Discord.
+ *
+ * @default false
+ */
+ showActiveEvent?: boolean;
+ /**
+ * Whether to show "Played for ... since ..." in Discord.
+ *
+ * @default true
+ */
+ showPlayTime?: boolean;
+
+ /**
+ * A function to call to customise the Discord activity.
+ */
+ callback?: (activity: DiscordRPC.Presence, game: Game, context?: DiscordPresenceContext) => void;
+}
+
+export enum DiscordPresencePlayTime {
+ /** Don't show play time */
+ HIDDEN,
+ /** "First played x minutes/hours/days ago" or "Played for [x5] hours or more" */
+ NINTENDO,
+ /** "Played for [x5] hours or more" */
+ APPROXIMATE_PLAY_TIME,
+ /** "Played for [x5] hours or more since dd/mm/yyyy" */
+ APPROXIMATE_PLAY_TIME_SINCE,
+ /** "Played for x hours and x minutes" */
+ DETAILED_PLAY_TIME,
+ /** "Played for x hours and x minutes since dd/mm/yyyy" */
+ DETAILED_PLAY_TIME_SINCE,
+}
diff --git a/src/discord/util.ts b/src/discord/util.ts
index f161a27..c7f4e14 100644
--- a/src/discord/util.ts
+++ b/src/discord/util.ts
@@ -1,9 +1,9 @@
import DiscordRPC from 'discord-rpc';
-import { ActiveEvent, CurrentUser, Friend, Game, PresenceState } from '../api/coral-types.js';
+import { Game, PresenceState } from '../api/coral-types.js';
import { defaultTitle, titles } from './titles.js';
import { product, version } from '../util/product.js';
import { getTitleIdFromEcUrl, hrduration } from '../util/misc.js';
-import { ZncDiscordPresence, ZncProxyDiscordPresence } from '../common/presence.js';
+import { DiscordPresence, DiscordPresenceContext, DiscordPresencePlayTime } from './types.js';
export function getDiscordPresence(
state: PresenceState, game: Game, context?: DiscordPresenceContext
@@ -142,22 +142,6 @@ export function getInactiveDiscordPresence(
};
}
-export interface DiscordPresenceContext {
- friendcode?: CurrentUser['links']['friendCode'];
- activeevent?: ActiveEvent;
- show_play_time?: DiscordPresencePlayTime;
- znc_discord_presence?: ZncDiscordPresence | ZncProxyDiscordPresence;
- nsaid?: string;
- user?: CurrentUser | Friend;
-}
-
-export interface DiscordPresence {
- id: string;
- title: string | null;
- activity: DiscordRPC.Presence;
- showTimestamp?: boolean;
-}
-
export function getTitleConfiguration(game: Game, id: string) {
return titles.find(title => {
if (title.id !== id) return false;
@@ -165,96 +149,3 @@ export function getTitleConfiguration(game: Game, id: string) {
return true;
});
}
-
-type SystemModuleTitleId = `01000000000000${string}`;
-type SystemDataTitleId = `01000000000008${string}`;
-type SystemAppletTitleId = `0100000000001${string}`;
-type ApplicationTitleId = `0100${string}${'0' | '2' | '4' | '6' | '8' | 'a' | 'c' | 'e'}000`;
-
-export interface Title {
- /**
- * Lowercase hexadecimal title ID.
- *
- * Valid application title IDs are 16 characters long, and should start with `0100` and end with `0000`, `2000`, `4000`, `6000`, `8000`, `a000`, `c000`, `e000` (this is because applications have 16^4 title IDs for the application itself, plus addon content and update data).
- */
- id: ApplicationTitleId | '0000000000000000';
- /**
- * Discord client ID
- */
- client: string;
-
- /**
- * Title name to show in Discord. This is *not* the name that will appear under the user's name after "Playing ".
- *
- * If this is set to true the title's name from znc will be used.
- * If this is set to false (default) no title name will be set. This should be used when a specific Discord client for the title is used.
- * If this is set to a string it will be used as the title name.
- *
- * @default false
- */
- titleName?: string | boolean;
- /**
- * By default the title's icon from znc will be used. (No icons need to be uploaded to Discord.)
- */
- largeImageKey?: string;
- largeImageText?: string;
- /**
- * By default the user's icon and friend code will be used if the user is sharing their friend code; otherwise it will not be set.
- */
- smallImageKey?: string;
- smallImageText?: string;
- /**
- * Whether to show the timestamp the user started playing the title in Discord. Discord shows this as the number of minutes and seconds since the timestamp.
- *
- * If enabled this is set to the time the user's presence was last updated as reported by Nintendo. Any changes to the updated timestamp will be ignored as long as the title doesn't change. The timestamp may change if the presence tracking is reset for any reason.
- *
- * This is now enabled by default as it's required for the activity to show in the Active Now panel.
- *
- * @default true
- */
- showTimestamp?: boolean;
- /**
- * Show the activity description set by the title.
- *
- * @default true
- */
- showDescription?: boolean;
- /**
- * Show "Playing online" if playing online and the game doesn't set activity details.
- *
- * @default false
- */
- showPlayingOnline?: string | boolean;
- /**
- * Whether to show details of the current event (Online Lounge/voice chat) in Discord.
- *
- * @default false
- */
- showActiveEvent?: boolean;
- /**
- * Whether to show "Played for ... since ..." in Discord.
- *
- * @default true
- */
- showPlayTime?: boolean;
-
- /**
- * A function to call to customise the Discord activity.
- */
- callback?: (activity: DiscordRPC.Presence, game: Game, context?: DiscordPresenceContext) => void;
-}
-
-export enum DiscordPresencePlayTime {
- /** Don't show play time */
- HIDDEN,
- /** "First played x minutes/hours/days ago" or "Played for [x5] hours or more" */
- NINTENDO,
- /** "Played for [x5] hours or more" */
- APPROXIMATE_PLAY_TIME,
- /** "Played for [x5] hours or more since dd/mm/yyyy" */
- APPROXIMATE_PLAY_TIME_SINCE,
- /** "Played for x hours and x minutes" */
- DETAILED_PLAY_TIME,
- /** "Played for x hours and x minutes since dd/mm/yyyy" */
- DETAILED_PLAY_TIME_SINCE,
-}