diff --git a/src/components/Settings.vue b/src/components/Settings.vue index 2a3bb8dd..444063c5 100644 --- a/src/components/Settings.vue +++ b/src/components/Settings.vue @@ -110,6 +110,10 @@ > {{ folder.name }} + + + {{ t('memories', 'Run initial device setup') }} + @@ -296,6 +300,10 @@ export default defineComponent({ nativex.setLocalFolders(this.localFolders); }, + runNxSetup() { + this.$router.replace('/nxsetup'); + }, + async logout() { if ( await utils.confirmDestructive({ diff --git a/src/native/Setup.vue b/src/native/Setup.vue index 538448b0..0a4448f5 100644 --- a/src/native/Setup.vue +++ b/src/native/Setup.vue @@ -28,7 +28,36 @@ -
+
+ {{ + t( + 'memories', + 'Memories can show local media on your device alongside the media on your server. This requires access to the media on this device.' + ) + }} +

+ + {{ + hasMediaPermission + ? t('memories', 'Access to media has been granted.') + : t( + 'memories', + 'Access to media is not available yet. If the button below does not work, grant the permission through settings.' + ) + }} + +
+ + {{ t('memories', 'Grant permissions') }} + + + + {{ hasMediaPermission ? t('memories', 'Continue') : t('memories', 'Skip this step') }} + +
+
+ +
{{ t('memories', 'Choose the folders on this device to show on your timeline.') }} {{ t( @@ -41,15 +70,22 @@
- - {{ folder.name }} - +
+ {{ t('memories', 'Synchronizing local files ({n} done).', { n: syncStatus }) }} +
+ {{ t('memories', 'This may take a while. Do not close this window.') }} +
+
@@ -82,17 +118,23 @@ export default defineComponent({ data: () => ({ banner, + hasMediaPermission: false, step: util.uid ? 1 : 0, localFolders: [] as nativex.LocalFolderConfig[], + syncStatus: -1, + syncStatusWatch: 0, }), watch: { step() { switch (this.step) { case 2: - this.localFolders = nativex.getLocalFolders(); + this.hasMediaPermission = nativex.configHasMediaPermission(); break; case 3: + this.localFolders = nativex.getLocalFolders(); + break; + case 4: this.$router.push('/'); break; } @@ -110,16 +152,36 @@ export default defineComponent({ // set nativex theme nativex.setTheme(getComputedStyle(document.body).getPropertyValue('--color-background-plain')); + + // set up sync status watcher + this.syncStatusWatch = window.setInterval(() => { + if (this.hasMediaPermission && this.step === 3) { + const newStatus = nativex.nativex.getSyncStatus(); + + // Refresh local folders if newly reached state -1 + if (newStatus === -1 && this.syncStatus !== -1) { + this.localFolders = nativex.getLocalFolders(); + } + + this.syncStatus = newStatus; + } + }, 500); }, beforeDestroy() { nativex.setTheme(); // reset theme + window.clearInterval(this.syncStatusWatch); }, methods: { updateDeviceFolders() { nativex.setLocalFolders(this.localFolders); }, + + async grantMediaPermission() { + await nativex.configAllowMedia(); + this.hasMediaPermission = nativex.configHasMediaPermission(); + }, }, }); diff --git a/src/native/api.ts b/src/native/api.ts index 144d6909..bffdd15c 100644 --- a/src/native/api.ts +++ b/src/native/api.ts @@ -77,6 +77,13 @@ export const NAPI = { * @returns {void} */ SHARE_LOCAL: (auid: number) => `${BASE_URL}/api/share/local/${auid}`, + + /** + * Allow usage of local media (permissions request) + * @param val Allow or disallow media + * @returns + */ + CONFIG_ALLOW_MEDIA: (val: boolean) => `${BASE_URL}/api/config/allow_media/${val ? '1' : '0'}`, }; /** NativeX synchronous API. */ @@ -159,6 +166,18 @@ export type NativeX = { * @returns JSON-encoded array of LocalFolderConfig */ configGetLocalFolders: () => string; + + /** + * Check if the user has allowed media access. + * @returns Whether the user has allowed media access. + */ + configHasMediaPermission: () => boolean; + + /** + * Get the current sync status. + * @returns number of file synced or -1 + */ + getSyncStatus: () => number; }; /** The native interface is a global object that is injected by the native app. */ diff --git a/src/native/config.ts b/src/native/config.ts index fbbd6247..b5408e2c 100644 --- a/src/native/config.ts +++ b/src/native/config.ts @@ -1,4 +1,4 @@ -import { nativex } from './api'; +import { NAPI, nativex } from './api'; /** Setting of whether a local folder is enabled */ export type LocalFolderConfig = { @@ -21,3 +21,17 @@ export function setLocalFolders(config: LocalFolderConfig[]) { export function getLocalFolders() { return JSON.parse(nativex?.configGetLocalFolders?.() ?? '[]') as LocalFolderConfig[]; } + +/** + * Check if the user has allowed media access. + */ +export function configHasMediaPermission() { + return nativex?.configHasMediaPermission?.() ?? false; +} + +/** + * Allow access to media. + */ +export async function configAllowMedia(val: boolean = true) { + return await fetch(NAPI.CONFIG_ALLOW_MEDIA(val)); +} diff --git a/src/native/days.ts b/src/native/days.ts index e82108df..ab2b7328 100644 --- a/src/native/days.ts +++ b/src/native/days.ts @@ -1,4 +1,3 @@ -import axios from '@nextcloud/axios'; import { NAPI } from './api'; import { API } from '../services/API'; import { has } from './basic'; @@ -70,6 +69,11 @@ export async function deleteLocalPhotos(photos: IPhoto[], dry: boolean = false): if (!has()) return 0; const auids = photos.map((p) => p.auid).filter((a) => !!a) as number[]; - const res = await axios.get(API.Q(NAPI.IMAGE_DELETE(auids), { dry })); - return res.data.confirms ? res.data.count : 0; + + // Delete local photos + const res = await fetch(API.Q(NAPI.IMAGE_DELETE(auids), { dry })); + if (!res.ok) throw new Error('Failed to delete photos'); + + const data = await res.json(); + return data.confirms ? data.count : 0; }