dexie: initial commit

Signed-off-by: Varun Patil <radialapps@gmail.com>
dexie
Varun Patil 2023-10-01 14:24:11 -07:00
parent f6ba121c40
commit b02bd2aaef
7 changed files with 243 additions and 68 deletions

14
package-lock.json generated
View File

@ -13,6 +13,7 @@
"@nextcloud/paths": "^2.1.0", "@nextcloud/paths": "^2.1.0",
"@nextcloud/sharing": "^0.1.0", "@nextcloud/sharing": "^0.1.0",
"@nextcloud/vue": "7.12.1", "@nextcloud/vue": "7.12.1",
"dexie": "^3.2.4",
"filerobot-image-editor": "^4.5.1", "filerobot-image-editor": "^4.5.1",
"fuse.js": "^6.6.2", "fuse.js": "^6.6.2",
"hammerjs": "^2.0.8", "hammerjs": "^2.0.8",
@ -4552,6 +4553,14 @@
"resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==" "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="
}, },
"node_modules/dexie": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/dexie/-/dexie-3.2.4.tgz",
"integrity": "sha512-VKoTQRSv7+RnffpOJ3Dh6ozknBqzWw/F3iqMdsZg958R0AS8AnY9x9d1lbwENr0gzeGJHXKcGhAMRaqys6SxqA==",
"engines": {
"node": ">=6.0"
}
},
"node_modules/diff": { "node_modules/diff": {
"version": "5.1.0", "version": "5.1.0",
"resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz",
@ -15026,6 +15035,11 @@
"resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==" "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="
}, },
"dexie": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/dexie/-/dexie-3.2.4.tgz",
"integrity": "sha512-VKoTQRSv7+RnffpOJ3Dh6ozknBqzWw/F3iqMdsZg958R0AS8AnY9x9d1lbwENr0gzeGJHXKcGhAMRaqys6SxqA=="
},
"diff": { "diff": {
"version": "5.1.0", "version": "5.1.0",
"resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz",

View File

@ -34,6 +34,7 @@
"@nextcloud/paths": "^2.1.0", "@nextcloud/paths": "^2.1.0",
"@nextcloud/sharing": "^0.1.0", "@nextcloud/sharing": "^0.1.0",
"@nextcloud/vue": "7.12.1", "@nextcloud/vue": "7.12.1",
"dexie": "^3.2.4",
"filerobot-image-editor": "^4.5.1", "filerobot-image-editor": "^4.5.1",
"fuse.js": "^6.6.2", "fuse.js": "^6.6.2",
"hammerjs": "^2.0.8", "hammerjs": "^2.0.8",

View File

@ -160,6 +160,7 @@ import { defineComponent } from 'vue';
import UserConfig from '../mixins/UserConfig'; import UserConfig from '../mixins/UserConfig';
import * as utils from '../services/utils'; import * as utils from '../services/utils';
import * as nativex from '../native'; import * as nativex from '../native';
import * as nativeSync from '../native/sync';
import NcButton from '@nextcloud/vue/dist/Components/NcButton'; import NcButton from '@nextcloud/vue/dist/Components/NcButton';
const NcAppSettingsDialog = () => import('@nextcloud/vue/dist/Components/NcAppSettingsDialog'); const NcAppSettingsDialog = () => import('@nextcloud/vue/dist/Components/NcAppSettingsDialog');
@ -169,6 +170,7 @@ const NcCheckboxRadioSwitch = () => import('@nextcloud/vue/dist/Components/NcChe
import MultiPathSelectionModal from './modal/MultiPathSelectionModal.vue'; import MultiPathSelectionModal from './modal/MultiPathSelectionModal.vue';
import type { IConfig } from '../types'; import type { IConfig } from '../types';
import type { LocalFolderConfig } from '../native/types';
export default defineComponent({ export default defineComponent({
name: 'Settings', name: 'Settings',
@ -184,7 +186,7 @@ export default defineComponent({
mixins: [UserConfig], mixins: [UserConfig],
data: () => ({ data: () => ({
localFolders: [] as nativex.LocalFolderConfig[], localFolders: [] as LocalFolderConfig[],
}), }),
props: { props: {
@ -289,11 +291,11 @@ export default defineComponent({
// --------------- Native APIs start ----------------------------- // --------------- Native APIs start -----------------------------
async refreshNativeConfig() { async refreshNativeConfig() {
this.localFolders = await nativex.getLocalFolders(); this.localFolders = await nativeSync.getLocalFolders();
}, },
async updateDeviceFolders() { async updateDeviceFolders() {
await nativex.setLocalFolders(this.localFolders); await nativeSync.setLocalFolders(this.localFolders);
}, },
async logout() { async logout() {

View File

@ -0,0 +1,10 @@
/**
* Add current origin to URL if doesn't have any protocol or origin.
*/
export function addOrigin(url: string) {
return url.match(/^(https?:)?\/\//)
? url
: url.startsWith('/')
? `${location.origin}${url}`
: `${location.origin}/${url}`;
}

View File

@ -1,7 +1,13 @@
import axios from '@nextcloud/axios'; import axios from '@nextcloud/axios';
import { generateUrl } from '@nextcloud/router'; import { generateUrl } from '@nextcloud/router';
import type { IDay, IPhoto } from './types';
import { API as SAPI } from './services/API'; import type { IDay, IPhoto } from '../types';
import { API as SAPI } from '../services/API';
import { addOrigin } from './helpers';
import type { LocalFolderConfig, ISystemImage } from './types';
import * as sync from './sync';
const euc = encodeURIComponent; const euc = encodeURIComponent;
/** Access NativeX over localhost */ /** Access NativeX over localhost */
@ -9,20 +15,6 @@ const BASE_URL = 'http://127.0.0.1';
/** NativeX asynchronous API */ /** NativeX asynchronous API */
export const API = { export const API = {
/**
* Local days API.
* @regex ^/api/days$
* @returns {IDay[]} for all locally available days.
*/
DAYS: () => `${BASE_URL}/api/days`,
/**
* Local photos API.
* @regex ^/api/days/\d+$
* @param dayId Day ID to fetch photos for
* @returns {IPhoto[]} for all locally available photos for this day.
*/
DAY: (dayId: number) => `${BASE_URL}/api/days/${dayId}`,
/** /**
* Local photo metadata API. * Local photo metadata API.
* @regex ^/api/image/info/\d+$ * @regex ^/api/image/info/\d+$
@ -81,13 +73,6 @@ export const API = {
* @returns {void} * @returns {void}
*/ */
SHARE_LOCAL: (auid: number) => `${BASE_URL}/api/share/local/${auid}`, SHARE_LOCAL: (auid: number) => `${BASE_URL}/api/share/local/${auid}`,
/**
* Get list of local folders configuration.
* @regex ^/api/config/local-folders$
* @returns {LocalFolderConfig[]} List of local folders configuration
*/
CONFIG_LOCAL_FOLDERS: () => `${BASE_URL}/api/config/local-folders`,
}; };
/** NativeX synchronous API. */ /** NativeX synchronous API. */
@ -142,12 +127,6 @@ export type NativeX = {
*/ */
destroyVideo: (fileid: string) => void; destroyVideo: (fileid: string) => void;
/**
* Set the local folders configuration to show in the timeline.
* @param json JSON-encoded array of LocalFolderConfig
*/
configSetLocalFolders: (json: string) => void;
/** /**
* Start the login process * Start the login process
* @param baseUrl Base URL of the Nextcloud instance * @param baseUrl Base URL of the Nextcloud instance
@ -164,13 +143,18 @@ export type NativeX = {
* Reload the app. * Reload the app.
*/ */
reload: () => void; reload: () => void;
};
/** Setting of whether a local folder is enabled */ /**
export type LocalFolderConfig = { * Initialize local sync.
id: string; * @param startTime Start time of the sync
name: string; */
enabled: boolean; localSyncInit: (startTime: number) => void;
/**
* Get the next batch of local sync.
* @returns {ISystemImage[]} Batch of photos to add
*/
localSyncNext: () => string;
}; };
/** The native interface is a global object that is injected by the native app. */ /** The native interface is a global object that is injected by the native app. */
@ -270,9 +254,7 @@ export async function extendDaysWithLocal(days: IDay[]) {
if (!has()) return; if (!has()) return;
// Query native part // Query native part
const res = await fetch(API.DAYS()); const local: IDay[] = await sync.getDaysLocal();
if (!res.ok) return;
const local: IDay[] = await res.json();
const remoteMap = new Map(days.map((d) => [d.dayid, d])); const remoteMap = new Map(days.map((d) => [d.dayid, d]));
// Merge local days into remote days // Merge local days into remote days
@ -301,12 +283,10 @@ export async function extendDaysWithLocal(days: IDay[]) {
export async function extendDayWithLocal(dayId: number, photos: IPhoto[]) { export async function extendDayWithLocal(dayId: number, photos: IPhoto[]) {
if (!has()) return; if (!has()) return;
// Query native part
const res = await fetch(API.DAY(dayId));
if (!res.ok) return;
// Merge local photos into remote photos // Merge local photos into remote photos
const localPhotos: IPhoto[] = await res.json(); const localPhotos: IPhoto[] = await sync.getDayLocal(dayId);
console.log('nativex', dayId, JSON.stringify(localPhotos));
const serverAUIDs = new Set(photos.map((p) => p.auid)); const serverAUIDs = new Set(photos.map((p) => p.auid));
// Filter out files that are only available locally // Filter out files that are only available locally
@ -332,21 +312,6 @@ export async function deleteLocalPhotos(photos: IPhoto[], dry: boolean = false):
return res.data.confirms ? res.data.count : 0; return res.data.confirms ? res.data.count : 0;
} }
/**
* Get list of local folders configuration.
* Should be called only if NativeX is available.
*/
export async function getLocalFolders() {
return (await axios.get<LocalFolderConfig[]>(API.CONFIG_LOCAL_FOLDERS())).data;
}
/**
* Set list of local folders configuration.
*/
export async function setLocalFolders(config: LocalFolderConfig[]) {
nativex?.configSetLocalFolders(JSON.stringify(config));
}
/** /**
* Log out from Nextcloud and pass ahead. * Log out from Nextcloud and pass ahead.
*/ */
@ -357,12 +322,28 @@ export async function logout() {
} }
/** /**
* Add current origin to URL if doesn't have any protocol or origin. * Iterate the local files on the device.
*/ */
function addOrigin(url: string) { export async function* localSyncIter(startTime: number) {
return url.match(/^(https?:)?\/\//) if (!has()) return;
? url
: url.startsWith('/') // Initialize and iterate
? `${location.origin}${url}` nativex.localSyncInit(startTime);
: `${location.origin}/${url}`;
while (true) {
const next = nativex.localSyncNext();
if (!next) break;
let photos: ISystemImage[];
try {
photos = JSON.parse(next);
} catch (e) {
console.error('Failed to parse local sync JSON', e);
continue;
}
for (const photo of photos) {
yield photo;
}
}
} }

144
src/native/sync.ts 100644
View File

@ -0,0 +1,144 @@
import Dexie from 'dexie';
import { getBuilder } from '@nextcloud/browser-storage';
import { localSyncIter } from '.';
import type { IDay, IPhoto } from '../types';
import { ISystemImage, LocalFolderConfig } from './types';
class MemoriesDatabase extends Dexie {
local!: Dexie.Table<ILocalFile, number>;
constructor() {
super('MemoriesDatabase');
this.version(1).stores({
local: '++id, fileid, auid, dayid, flag, [bucket_id+dayid]',
});
}
}
/** Table for a locally stored file. */
interface ILocalFile extends ISystemImage {
id?: number;
dayid: number;
flag: number;
}
/** Open database */
const db = new MemoriesDatabase();
/** Preferences database */
const storage = getBuilder('memories_sync').clearOnLogout(false).persist().build();
const STORAGE_LOCAL_FOLDERS = 'local_folders';
// Cache for preferences
let _enabledBucketIds: number[] | null = null;
/**
* Trigger the synchronization process at the local database.
*/
export async function go() {
// Clear local database
// await db.local.clear();
for await (const sysImg of localSyncIter(0)) {
// Check if file already exists with same mtime
if (await db.local.where({ fileid: sysImg.fileid, mtime: sysImg.mtime }).first()) continue;
// Insert new file
await db.transaction('rw', db.local, async () => {
await db.local.where({ fileid: sysImg.fileid }).delete();
await db.local.add({
...sysImg,
dayid: Math.floor(sysImg.datetaken / 86400),
flag: 0,
});
});
}
}
/**
* Get the local days list.
* @param dayid Day ID
*/
export async function getDaysLocal(): Promise<IDay[]> {
if (!_enabledBucketIds?.length) return [];
const days: IDay[] = [];
// Get all day IDs
const dayIds = await db.local.orderBy('dayid').uniqueKeys();
// For each unique username, call usePostCountSince():
await Promise.all(
dayIds.map(async (dayid: number) => {
days.push({
dayid: dayid,
count: await db.local
.where(['bucket_id', 'dayid'])
.anyOf(getEnabledBucketIds().map((bucket_id) => [bucket_id, dayid]))
.count(),
});
})
);
return days.filter((d) => d.count > 0);
}
/**
* Get the local photos for a given day.
* @param dayid Day ID
*/
export async function getDayLocal(dayid: number): Promise<IPhoto[]> {
if (!_enabledBucketIds?.length) return [];
return (
await db.local
.where(['bucket_id', 'dayid'])
.anyOf(getEnabledBucketIds().map((bucket_id) => [bucket_id, dayid]))
.toArray()
).map((file) => ({
...file,
bucket_id: undefined,
bucket_name: undefined,
flag: undefined as unknown as number,
}));
}
/** Get the enabled bucket ids list */
export function getEnabledBucketIds(): number[] {
if (_enabledBucketIds) return _enabledBucketIds;
try {
_enabledBucketIds = JSON.parse(storage.getItem(STORAGE_LOCAL_FOLDERS) || '[]') as number[];
} catch (e) {
_enabledBucketIds = [];
}
return _enabledBucketIds;
}
/** Get the active bucket list. */
export async function getLocalFolders(): Promise<LocalFolderConfig[]> {
const bucketIds = await db.local.orderBy('bucket_id').uniqueKeys();
const enabledIds = getEnabledBucketIds();
const buckets = await Promise.all(
bucketIds.map(async (bucket_id: number) => {
const file = await db.local.where({ bucket_id }).first();
if (!file) return null;
return {
id: file.bucket_id,
name: file.bucket_name,
enabled: enabledIds.includes(file.bucket_id),
};
})
);
return buckets.filter((b) => b !== null) as LocalFolderConfig[];
}
export async function setLocalFolders(config: LocalFolderConfig[]) {
_enabledBucketIds = config.filter((f) => f.enabled).map((f) => f.id);
storage.setItem(STORAGE_LOCAL_FOLDERS, JSON.stringify(_enabledBucketIds));
}

View File

@ -0,0 +1,23 @@
/** Local file type */
export type ISystemImage = {
fileid: number;
basename: string;
mimetype: string;
h: number;
w: number;
size: number;
etag: string;
mtime: number;
epoch: number;
auid: number;
bucket_id: number;
bucket_name: string;
datetaken: number;
};
/** Setting of whether a local folder is enabled */
export type LocalFolderConfig = {
id: number;
name: string;
enabled: boolean;
};