Show share menu for web services

This commit is contained in:
Samuel Elliott
2022-05-29 18:02:07 +01:00
parent 551daa868a
commit 5328ada1e7
8 changed files with 95 additions and 46 deletions

View File

@@ -266,7 +266,7 @@ function getEnvApi(): FApi {
throw new Error('Unknown znca API provider');
}
if (process.env.ZNCA_API_URL?.startsWith('https://')) {
if (process.env.ZNCA_API_URL) {
return ['nxapi', process.env.ZNCA_API_URL + '/f'];
}

View File

@@ -12,6 +12,7 @@ export const MenuItem = electron.MenuItem;
export const nativeImage = electron.nativeImage;
export const Notification = electron.Notification;
export const session = electron.session;
export const ShareMenu = electron.ShareMenu;
export const shell = electron.shell;
export const Tray = electron.Tray;
@@ -20,5 +21,6 @@ export type IpcMainInvokeEvent = import('electron').IpcMainInvokeEvent;
export type Menu = import('electron').Menu;
export type MenuItem = import('electron').MenuItem;
export type Notification = import('electron').Notification;
export type ShareMenu = import('electron').ShareMenu;
export type Tray = import('electron').Tray;
export type WebContents = import('electron').WebContents;

View File

@@ -1,9 +1,8 @@
import { app, BrowserWindow, dialog, ipcMain, nativeImage, Notification } from './electron.js';
import { app, BrowserWindow, dialog, ipcMain, Notification } from './electron.js';
import * as path from 'path';
import { EventEmitter } from 'events';
import createDebug from 'debug';
import * as persist from 'node-persist';
import fetch from 'node-fetch';
import dotenv from 'dotenv';
import dotenvExpand from 'dotenv-expand';
import MenuApp from './menu.js';
@@ -18,6 +17,7 @@ import { getToken } from '../../common/auth/nso.js';
import { dir } from '../../util/product.js';
import { initStorage, paths } from '../../util/storage.js';
import { LoopResult } from '../../util/loop.js';
import { tryGetNativeImageFromUrl } from './util.js';
const debug = createDebug('app:main');
@@ -251,16 +251,6 @@ export class EmbeddedPresenceMonitor extends ZncDiscordPresence {
}
export class ElectronNotificationManager extends NotificationManager {
private async getNativeImageFromUrl(url: string) {
try {
const response = await fetch(url);
const image = await response.buffer();
return nativeImage.createFromBuffer(image);
} catch (err) {}
return undefined;
}
async onFriendOnline(friend: CurrentUser | Friend, prev?: CurrentUser | Friend, naid?: string, ir?: boolean) {
const currenttitle = friend.presence.game as Game;
@@ -268,7 +258,7 @@ export class ElectronNotificationManager extends NotificationManager {
title: friend.name,
body: 'Playing ' + currenttitle.name +
(currenttitle.sysDescription ? '\n' + currenttitle.sysDescription : ''),
icon: await this.getNativeImageFromUrl(friend.imageUri),
icon: await tryGetNativeImageFromUrl(friend.imageUri),
}).show();
}
@@ -276,7 +266,7 @@ export class ElectronNotificationManager extends NotificationManager {
new Notification({
title: friend.name,
body: 'Offline',
icon: await this.getNativeImageFromUrl(friend.imageUri),
icon: await tryGetNativeImageFromUrl(friend.imageUri),
}).show();
}
@@ -287,7 +277,7 @@ export class ElectronNotificationManager extends NotificationManager {
title: friend.name,
body: 'Playing ' + currenttitle.name +
(currenttitle.sysDescription ? '\n' + currenttitle.sysDescription : ''),
icon: await this.getNativeImageFromUrl(friend.imageUri),
icon: await tryGetNativeImageFromUrl(friend.imageUri),
}).show();
}
@@ -298,7 +288,7 @@ export class ElectronNotificationManager extends NotificationManager {
title: friend.name,
body: 'Playing ' + currenttitle.name +
(currenttitle.sysDescription ? '\n' + currenttitle.sysDescription : ''),
icon: await this.getNativeImageFromUrl(friend.imageUri),
icon: await tryGetNativeImageFromUrl(friend.imageUri),
}).show();
}
}

View File

@@ -1,14 +1,14 @@
import * as crypto from 'crypto';
import createDebug from 'debug';
import * as persist from 'node-persist';
import fetch from 'node-fetch';
import { BrowserWindow, nativeImage, Notification, session, shell } from './electron.js';
import { BrowserWindow, Notification, session, shell } from './electron.js';
import { getNintendoAccountSessionToken, NintendoAccountSessionToken } from '../../api/na.js';
import { ZNCA_CLIENT_ID } from '../../api/znc.js';
import { ZNMA_CLIENT_ID } from '../../api/moon.js';
import { getToken, SavedToken } from '../../common/auth/nso.js';
import { getPctlToken, SavedMoonToken } from '../../common/auth/moon.js';
import { Jwt } from '../../util/jwt.js';
import { tryGetNativeImageFromUrl } from './util.js';
const debug = createDebug('app:main:na-auth');
@@ -157,18 +157,10 @@ export async function addNsoAccount(storage: persist.LocalStorage) {
debug('Already authenticated', data);
let icon = undefined;
try {
const response = await fetch(data!.nsoAccount.user.imageUri);
const image = await response.buffer();
icon = nativeImage.createFromBuffer(image);
} catch (err) {}
new Notification({
title: 'Nintendo Switch Online',
body: 'Already signed in as ' + data?.nsoAccount.user.name + ' (' + data?.user.nickname + ')',
icon,
icon: await tryGetNativeImageFromUrl(data!.nsoAccount.user.imageUri),
}).show();
return getToken(storage, nsotoken, process.env.ZNC_PROXY_URL);
@@ -184,18 +176,10 @@ export async function addNsoAccount(storage: persist.LocalStorage) {
users.add(data.user.id);
await storage.setItem('NintendoAccountIds', [...users]);
let icon = undefined;
try {
const response = await fetch(data.nsoAccount.user.imageUri);
const image = await response.buffer();
icon = nativeImage.createFromBuffer(image);
} catch (err) {}
new Notification({
title: 'Nintendo Switch Online',
body: 'Authenticated as ' + data.nsoAccount.user.name + ' (NSO ' + data.user.nickname + ')',
icon,
icon: await tryGetNativeImageFromUrl(data.nsoAccount.user.imageUri),
}).show();
return {nso, data};

17
src/app/main/util.ts Normal file
View File

@@ -0,0 +1,17 @@
import { nativeImage } from './electron.js';
import { Buffer } from 'buffer';
import fetch from 'node-fetch';
export async function getNativeImageFromUrl(url: URL | string) {
const response = await fetch(url.toString());
const image = await response.arrayBuffer();
return nativeImage.createFromBuffer(Buffer.from(image));
}
export async function tryGetNativeImageFromUrl(url: URL | string) {
try {
return await getNativeImageFromUrl(url);
} catch (err) {}
return undefined;
}

View File

@@ -1,6 +1,10 @@
import * as path from 'path';
import { constants } from 'fs';
import * as fs from 'fs/promises';
import { Buffer } from 'buffer';
import createDebug from 'debug';
import { BrowserWindow, IpcMainInvokeEvent, session, shell, WebContents } from './electron.js';
import { app, BrowserWindow, IpcMainInvokeEvent, session, ShareMenu, shell, WebContents } from './electron.js';
import fetch from 'node-fetch';
import ZncApi from '../../api/znc.js';
import { dev } from '../../util/product.js';
import { WebService } from '../../api/znc-types.js';
@@ -149,6 +153,51 @@ export class WebServiceIpc {
const data: NativeShareRequest = JSON.parse(json);
debug('invokeNativeShare', webservice.name, nsoAccount.user.name, data);
const texts: string[] = [];
if (data.text) texts.push(data.text);
if (data.hashtags) texts.push(data.hashtags.map(t => '#' + t).join(' '));
const imagepath = await this.downloadShareImage(data);
const menu = new ShareMenu({
texts,
filePaths: [imagepath],
});
menu.popup({window: BrowserWindow.fromWebContents(event.sender)!});
}
private async downloadShareImage(req: NativeShareRequest) {
const dir = app.getPath('downloads');
const basename = path.basename(new URL(req.image_url).pathname);
const extname = path.extname(basename);
let filename;
let i = 0;
do {
i++;
filename = i === 1 ? basename : basename.substr(0, basename.length - extname.length) + ' ' + i + extname;
} while (await this.pathExists(path.join(dir, filename)));
debug('Downloading image %s to %s as %s', req.image_url, dir, filename);
const response = await fetch(req.image_url);
const image = await response.arrayBuffer();
await fs.writeFile(path.join(dir, filename), Buffer.from(image));
return path.join(dir, filename);
}
private async pathExists(path: string) {
try {
await fs.access(path, constants.F_OK);
return true;
} catch (err) {
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') throw err;
return false;
}
}
async invokeNativeShareUrl(event: IpcMainInvokeEvent, json: string): Promise<void> {
@@ -157,6 +206,13 @@ export class WebServiceIpc {
const data: NativeShareUrlRequest = JSON.parse(json);
debug('invokeNativeShareUrl', webservice.name, nsoAccount.user.name, data);
const menu = new ShareMenu({
texts: [data.text],
urls: [data.url],
});
menu.popup({window: BrowserWindow.fromWebContents(event.sender)!});
}
async requestGameWebToken(event: IpcMainInvokeEvent): Promise<string> {

View File

@@ -99,7 +99,7 @@ export async function dumpProfileImage(
debug('Fetching profile image');
const image_response = await fetch(share.url);
const image = await image_response.buffer();
const image = await image_response.arrayBuffer();
debug('Writing profile image data %s', filename);
await fs.writeFile(file, JSON.stringify({
@@ -109,7 +109,7 @@ export async function dumpProfileImage(
}, null, 4) + '\n', 'utf-8');
debug('Writing profile image %s', image_filename);
await fs.writeFile(image_file, image);
await fs.writeFile(image_file, Buffer.from(image));
await fs.writeFile(latest_file, JSON.stringify({timestamp, etag, etag_data}, null, 4) + '\n', 'utf-8');
}
@@ -136,13 +136,13 @@ export async function dumpChallenges(
debug('Fetching challenge image for %s', challenge.key);
const image_response = await fetch(share.url);
const image = await image_response.buffer();
const image = await image_response.arrayBuffer();
debug('Writing challenge image data %s', filename);
await fs.writeFile(file, JSON.stringify({share}, null, 4) + '\n', 'utf-8');
debug('Writing challenge image %s', filename);
await fs.writeFile(image_file, image);
await fs.writeFile(image_file, Buffer.from(image));
}
}

View File

@@ -46,7 +46,7 @@ export async function dumpResults(
debug('Fetching battle results summary image');
const image_response = await fetch(share.url);
const image = await image_response.buffer();
const image = await image_response.arrayBuffer();
debug('Writing battle results summary image data %s', filename);
await fs.writeFile(file, JSON.stringify({
@@ -54,7 +54,7 @@ export async function dumpResults(
}, null, 4) + '\n', 'utf-8');
debug('Writing battle results summary image %s', filename);
await fs.writeFile(image_file, image);
await fs.writeFile(image_file, Buffer.from(image));
}
const skipped = [];
@@ -103,7 +103,7 @@ export async function dumpResults(
debug('Fetching battle results image');
const image_response = await fetch(share.url);
const image = await image_response.buffer();
const image = await image_response.arrayBuffer();
debug('Writing battle results image data %s', filename);
await fs.writeFile(file, JSON.stringify({
@@ -111,7 +111,7 @@ export async function dumpResults(
}, null, 4) + '\n', 'utf-8');
debug('Writing battle results image %s', filename);
await fs.writeFile(image_file, image);
await fs.writeFile(image_file, Buffer.from(image));
}
}
}