nx: add more interfaces

Signed-off-by: Varun Patil <radialapps@gmail.com>
pull/653/head
Varun Patil 2023-05-03 19:59:23 -07:00
parent b889c5f5f7
commit 917fccf0da
12 changed files with 198 additions and 9 deletions

View File

@ -176,6 +176,13 @@ class DaysController extends GenericApiController
*/ */
private function preloadDays(array &$days) private function preloadDays(array &$days)
{ {
// Do not preload anything for native clients.
// Since the contents of preloads are trusted, clients will not load locals.
if (Util::callerIsNative()) {
return;
}
// Build identical transforms for sub queries
$transforms = $this->getTransformations(false); $transforms = $this->getTransformations(false);
$preloaded = 0; $preloaded = 0;
$preloadDayIds = []; $preloadDayIds = [];

View File

@ -59,8 +59,7 @@ class PageController extends Controller
$response->cacheFor(0); $response->cacheFor(0);
// Check if requested from native app // Check if requested from native app
$userAgent = $this->request->getHeader('User-Agent'); if (!Util::callerIsNative()) {
if (false === strpos($userAgent, 'memories-native')) {
$this->eventDispatcher->dispatchTyped(new LoadSidebar()); $this->eventDispatcher->dispatchTyped(new LoadSidebar());
} }

View File

@ -446,6 +446,17 @@ class Util
return self::getSystemConfig('instanceid', 'default', true); return self::getSystemConfig('instanceid', 'default', true);
} }
/**
* Checks if the API call was made from a native interface.
*/
public static function callerIsNative(): bool
{
$request = \OC::$server->get(\OCP\IRequest::class);
$userAgent = $request->getHeader('User-Agent');
return false !== strpos($userAgent, 'memories-native');
}
/** /**
* Kill all instances of a process by name. * Kill all instances of a process by name.
* Similar to pkill, which may not be available on all systems. * Similar to pkill, which may not be available on all systems.

View File

@ -65,6 +65,7 @@ import { translate as t } from '@nextcloud/l10n';
import { emit, subscribe } from '@nextcloud/event-bus'; import { emit, subscribe } from '@nextcloud/event-bus';
import * as utils from './services/Utils'; import * as utils from './services/Utils';
import * as nativex from './native';
import UserConfig from './mixins/UserConfig'; import UserConfig from './mixins/UserConfig';
import Timeline from './components/Timeline.vue'; import Timeline from './components/Timeline.vue';
import Settings from './components/Settings.vue'; import Settings from './components/Settings.vue';
@ -248,7 +249,7 @@ export default defineComponent({
); );
// Check for native interface // Check for native interface
if (window.nativex?.isNative()) { if (nativex?.has()) {
document.body.classList.add('native'); document.body.classList.add('native');
} }
}, },

View File

@ -123,6 +123,7 @@ import TopMatter from './top-matter/TopMatter.vue';
import * as dav from '../services/DavRequests'; import * as dav from '../services/DavRequests';
import * as utils from '../services/Utils'; import * as utils from '../services/Utils';
import * as strings from '../services/strings'; import * as strings from '../services/strings';
import * as nativex from '../native';
import { API, DaysFilterType } from '../services/API'; import { API, DaysFilterType } from '../services/API';
@ -908,6 +909,16 @@ export default defineComponent({
dayMap.get(photo.dayid)!.push(photo); dayMap.get(photo.dayid)!.push(photo);
} }
// Get local images if we are running in native environment.
// Get them all together for each day here.
if (nativex.has()) {
const promises: Promise<void>[] = [];
for (const [dayId, photos] of dayMap) {
promises.push(nativex.extendDayWithLocal(dayId, photos));
}
await Promise.all(promises);
}
// Store cache asynchronously // Store cache asynchronously
// Do this regardless of whether the state has // Do this regardless of whether the state has
// changed since the data is already fetched // changed since the data is already fetched

View File

@ -1,5 +1,6 @@
import { API } from '../../services/API'; import { API } from '../../services/API';
import { workerImporter } from '../../worker'; import { workerImporter } from '../../worker';
import * as nativex from '../../native';
import type * as w from './XImgWorker'; import type * as w from './XImgWorker';
// Global web worker to fetch images // Global web worker to fetch images
@ -65,6 +66,13 @@ export async function fetchImage(url: string) {
let entry = BLOB_CACHE.get(url); let entry = BLOB_CACHE.get(url);
if (entry) return entry[1]; if (entry) return entry[1];
// Check if native image
if (nativex.IS_NATIVE_URL(url)) {
const dataUri = await nativex.getJpegDataUri(url);
BLOB_CACHE.set(url, [60, dataUri]);
return dataUri;
}
// Fetch image // Fetch image
const blobUrl = await importer<typeof w.fetchImageSrc>('fetchImageSrc')(url); const blobUrl = await importer<typeof w.fetchImageSrc>('fetchImageSrc')(url);

View File

@ -13,7 +13,6 @@ import { getRequestToken } from '@nextcloud/auth';
import type { Route } from 'vue-router'; import type { Route } from 'vue-router';
import type { IPhoto } from './types'; import type { IPhoto } from './types';
import type { NativeX } from './types-native';
import type PlyrType from 'plyr'; import type PlyrType from 'plyr';
import type videojsType from 'video.js'; import type videojsType from 'video.js';
@ -51,8 +50,6 @@ declare global {
var Plyr: typeof PlyrType; var Plyr: typeof PlyrType;
var videoClientId: string; var videoClientId: string;
var videoClientIdPersistent: string; var videoClientIdPersistent: string;
var nativex: NativeX | undefined;
} }
// Allow global access to the router // Allow global access to the router

144
src/native.ts 100644
View File

@ -0,0 +1,144 @@
import type { IPhoto } from './types';
/**
* Type of a native promise (this will be the exact type in Java).
*/
type NativePromise<T> = (call: string, arg: T) => void;
/**
* Native interface for the Android app.
*/
export type NativeX = {
isNative: () => boolean;
getLocalByDayId: NativePromise<string>;
getJpeg: NativePromise<string>;
};
/** The native interface is a global object that is injected by the native app. */
const nativex: NativeX = globalThis.nativex;
/** List of promises that are waiting for a native response. */
const nativePromises = new Map<string, object>();
/**
* Wraps a native function in a promise.
* JavascriptInterface doesn't support async functions, so we have to do this manually.
* The native function should call `window.nativexr(call, resolve, reject)` when it's done.
*
* @param fun Function to promisify
* @param binary Whether the response is binary (will not be decoded)
*/
function nativePromisify<A, T>(fun: NativePromise<A>, binary = false): (arg: A) => Promise<T> {
if (!fun) {
return () => {
return new Promise((_, reject) => {
reject('Native function not available');
});
};
}
return (arg: A) => {
return new Promise((resolve, reject) => {
const call = Math.random().toString(36).substring(7);
nativePromises.set(call, { resolve, reject, binary });
fun(call, arg);
});
};
}
/**
* Registers the global handler for native responses.
* This should be called by the native app when it's ready to resolve a promise.
*
* @param call ID passed to native function
* @param resolve Response from native function
* @param reject Rejection from native function
*/
globalThis.nativexr = (call: string, resolve?: string, reject?: string) => {
const promise = nativePromises.get(call);
if (!promise) {
console.error('No promise found for call', call);
return;
}
if (resolve !== undefined) {
if (!(promise as any).binary) resolve = window.atob(resolve);
(promise as any).resolve(resolve);
} else if (reject !== undefined) {
(promise as any).reject(window.atob(reject));
} else {
console.error('No resolve or reject found for call', call);
return;
}
nativePromises.delete(call);
};
/**
* @returns Whether the native interface is available.
*/
export const has = () => !!nativex;
/**
* Gets the local photos for a day with a dayId.
*
* @param dayId Day ID to get photos for
* @returns List of local photos (JSON string)
*/
const getLocalByDayId = nativePromisify<number, string>(nativex?.getLocalByDayId.bind(nativex));
/**
* Gets the JPEG data for a photo using a local URI.
*
* @param url Local URI to get JPEG data for
* @returns JPEG data (base64 string)
*/
const getJpeg = nativePromisify<string, string>(nativex?.getJpeg.bind(nativex), true);
/**
* Extend a list of photos with local photos.
* Fetches the local photos from the native interface and filters out duplicates.
*
* @param dayId Day ID to append local photos to
* @param photos List of photos to append to (duplicates will not be added)
* @returns
*/
export async function extendDayWithLocal(dayId: number, photos: IPhoto[]) {
if (!has()) return;
const localPhotos: IPhoto[] = JSON.parse(await getLocalByDayId(dayId));
const photosSet = new Set(photos.map((p) => p.basename));
const localOnly = localPhotos.filter((p) => !photosSet.has(p.basename));
localOnly.forEach((p) => (p.islocal = true));
photos.push(...localOnly);
}
/**
* Gets the JPEG data URI for a photo using a native URI.
*
* @param url Native URI to get JPEG data for
* @returns Data URI for JPEG
*/
export async function getJpegDataUri(url: string) {
const image = await getJpeg(url);
return `data:image/jpeg;base64,${image}`;
}
/**
* Checks whether a URL is a native URI (nativex://).
*
* @param url URL to check
*/
export function IS_NATIVE_URL(url: string) {
return url.startsWith('nativex://');
}
/**
* Get a downsized preview URL for a native file ID.
*
* @param fileid Local file ID returned by native interface
* @returns native URI
*/
export function NATIVE_URL_PREVIEW(fileid: number) {
return `nativex://preview/${fileid}`;
}

View File

@ -9,6 +9,7 @@ export const constants = {
FLAG_IS_FAVORITE: 1 << 3, FLAG_IS_FAVORITE: 1 << 3,
FLAG_SELECTED: 1 << 4, FLAG_SELECTED: 1 << 4,
FLAG_LEAVING: 1 << 5, FLAG_LEAVING: 1 << 5,
FLAG_IS_LOCAL: 1 << 6,
}, },
}; };
@ -30,4 +31,8 @@ export function convertFlags(photo: IPhoto) {
photo.flag |= constants.c.FLAG_IS_FAVORITE; photo.flag |= constants.c.FLAG_IS_FAVORITE;
delete photo.isfavorite; delete photo.isfavorite;
} }
if (photo.islocal) {
photo.flag |= constants.c.FLAG_IS_LOCAL;
delete photo.islocal;
}
} }

View File

@ -1,8 +1,15 @@
import { IImageInfo, IPhoto } from '../../types'; import { IImageInfo, IPhoto } from '../../types';
import { API } from '../API'; import { API } from '../API';
import { constants } from './const';
import * as nativex from '../../native';
/** Get preview URL from photo object */ /** Get preview URL from photo object */
export function getPreviewUrl(photo: IPhoto, square: boolean, size: number | [number, number] | 'screen') { export function getPreviewUrl(photo: IPhoto, square: boolean, size: number | [number, number] | 'screen') {
// Native preview
if (photo.flag & constants.c.FLAG_IS_LOCAL) {
return nativex.NATIVE_URL_PREVIEW(photo.fileid);
}
// Screen-appropriate size // Screen-appropriate size
if (size === 'screen') { if (size === 'screen') {
const sw = Math.floor(screen.width * devicePixelRatio); const sw = Math.floor(screen.width * devicePixelRatio);

View File

@ -1,3 +0,0 @@
export type NativeX = {
isNative: () => boolean;
};

View File

@ -71,6 +71,8 @@ export type IPhoto = {
video_duration?: number; video_duration?: number;
/** Favorite flag from server */ /** Favorite flag from server */
isfavorite?: boolean; isfavorite?: boolean;
/** Local file from native */
islocal?: boolean;
/** Optional datetaken epoch */ /** Optional datetaken epoch */
datetaken?: number; datetaken?: number;
}; };