refactor(fragment): support multiple frags

Signed-off-by: Varun Patil <radialapps@gmail.com>
pull/888/head
Varun Patil 2023-10-23 22:12:48 -07:00
parent 182caed840
commit 557e87ca3c
9 changed files with 194 additions and 63 deletions

View File

@ -28,6 +28,7 @@ globalThis._m = {
get route() { get route() {
return router.currentRoute; return router.currentRoute;
}, },
router: router,
routes: routes, routes: routes,
modals: {} as any, modals: {} as any,

View File

@ -867,7 +867,7 @@ export default defineComponent({
/** Open viewer with given photo */ /** Open viewer with given photo */
openViewer(photo: IPhoto) { openViewer(photo: IPhoto) {
nativex.playTouchSound(); nativex.playTouchSound();
this.$router.push(utils.getViewerRoute(photo)); _m.viewer.open(photo);
}, },
}, },
}); });

View File

@ -269,14 +269,9 @@ export default defineComponent({
await this.$nextTick(); await this.$nextTick();
// Check if hash has changed // Check if hash has changed
if (from?.hash !== to.hash && to.hash?.startsWith('#v') && !_m.viewer.isOpen) { if (from?.hash !== to.hash && !_m.viewer.isOpen && utils.fragment.viewer.open) {
// Open viewer // Open viewer
const parts = to.hash.split('/'); const { dayid, key } = utils.fragment.viewer;
if (parts.length !== 3) return;
// Get params
const dayid = parseInt(parts[1]);
const key = parts[2];
if (isNaN(dayid) || !key) return; if (isNaN(dayid) || !key) return;
// Get day // Get day
@ -299,9 +294,9 @@ export default defineComponent({
} }
} }
_m.viewer.open(photo, this.list); _m.viewer.openDynamic(photo, this.list);
} else if (!to.hash?.startsWith('#v') && _m.viewer.isOpen) { } else if (!utils.fragment.viewer.open && _m.viewer.isOpen) {
// Close viewer // No viewer fragment but viewer is open
_m.viewer.close(); _m.viewer.close();
} }
}, },
@ -693,7 +688,7 @@ export default defineComponent({
data = await dav.getOnThisDayData(); data = await dav.getOnThisDayData();
} else if (dav.isSingleItem()) { } else if (dav.isSingleItem()) {
data = await dav.getSingleItemData(); data = await dav.getSingleItemData();
this.$router.replace(utils.getViewerRoute(data[0]!.detail![0])); _m.viewer.open(data[0]!.detail![0]);
} else { } else {
// Try the cache // Try the cache
if (!noCache) { if (!noCache) {

View File

@ -298,7 +298,7 @@ export default defineComponent({
// At high zoom levels, open the photo // At high zoom levels, open the photo
if (this.zoom >= 12 && cluster.preview) { if (this.zoom >= 12 && cluster.preview) {
cluster.preview.key = cluster.preview.fileid.toString(); cluster.preview.key = cluster.preview.fileid.toString();
this.$router.push(utils.getViewerRoute(cluster.preview)); _m.viewer.open(cluster.preview);
return; return;
} }

View File

@ -287,7 +287,8 @@ export default defineComponent({
// The viewer is a singleton // The viewer is a singleton
const self = this; const self = this;
_m.viewer = { _m.viewer = {
open: this.open.bind(this) as typeof this.open, open: this.setFragment.bind(this) as typeof this.setFragment,
openDynamic: this.openDynamic.bind(this) as typeof this.openDynamic,
openStatic: this.openStatic.bind(this) as typeof this.openStatic, openStatic: this.openStatic.bind(this) as typeof this.openStatic,
close: this.close.bind(this) as typeof this.close, close: this.close.bind(this) as typeof this.close,
get isOpen() { get isOpen() {
@ -553,7 +554,7 @@ export default defineComponent({
this.fullyOpened = false; this.fullyOpened = false;
this.setUiVisible(false); this.setUiVisible(false);
this.hideSidebar(); this.hideSidebar();
this.setRouteHash(undefined); this.setFragment(null);
this.updateTitle(undefined); this.updateTitle(undefined);
nativex.setTheme(); // reset nativex.setTheme(); // reset
document.body.classList.remove(BODY_VIEWER_VIDEO); document.body.classList.remove(BODY_VIEWER_VIDEO);
@ -581,7 +582,7 @@ export default defineComponent({
this.photoswipe.on('slideActivate', (e) => { this.photoswipe.on('slideActivate', (e) => {
this.currIndex = this.photoswipe!.currIndex; this.currIndex = this.photoswipe!.currIndex;
const photo = e.slide?.data?.photo; const photo = e.slide?.data?.photo;
this.setRouteHash(photo); this.setFragment(photo);
this.updateTitle(photo); this.updateTitle(photo);
}); });
@ -636,7 +637,7 @@ export default defineComponent({
}, },
/** Open using start photo and rows list */ /** Open using start photo and rows list */
async open(anchorPhoto: IPhoto, rows: IRow[]) { async openDynamic(anchorPhoto: IPhoto, rows: IRow[]) {
const detail = anchorPhoto.d?.detail; const detail = anchorPhoto.d?.detail;
if (!detail) { if (!detail) {
console.error('Attempted to open viewer with no detail list!'); console.error('Attempted to open viewer with no detail list!');
@ -865,33 +866,16 @@ export default defineComponent({
}, },
/** Set the route hash to the given photo */ /** Set the route hash to the given photo */
setRouteHash(photo: IPhoto | undefined) { setFragment(photo: IPhoto | null) {
if (!photo) { if (photo) {
if (!this.isOpen && this.$route.hash?.startsWith('#v')) { const frag = utils.fragment.viewer;
this.$router.back(); frag.dayid = photo.dayid;
frag.key = photo.key!;
// Ensure this does not have the hash, otherwise replace it return utils.fragment.push(frag);
if (this.$route.hash?.startsWith('#v')) {
this.$router.replace({
hash: '',
query: this.$route.query,
});
}
}
return;
} }
const hash = photo ? utils.getViewerHash(photo) : '';
const route = { if (!this.isOpen) {
path: this.$route.path, return utils.fragment.pop('v');
query: this.$route.query,
hash,
};
if (hash !== this.$route.hash) {
if (this.$route.hash) {
this.$router.replace(route);
} else {
this.$router.push(route);
}
} }
}, },

6
src/globals.d.ts vendored
View File

@ -1,4 +1,4 @@
import type { Route } from 'vue-router'; import Router, { Route } from 'vue-router';
import type { ComponentPublicInstance } from 'vue'; import type { ComponentPublicInstance } from 'vue';
import type { translate, translatePlural } from '@nextcloud/l10n'; import type { translate, translatePlural } from '@nextcloud/l10n';
@ -32,6 +32,7 @@ declare global {
var _m: { var _m: {
mode: 'admin' | 'user'; mode: 'admin' | 'user';
route: Route; route: Route;
router: Router;
routes: typeof routes; routes: typeof routes;
modals: { modals: {
@ -52,7 +53,8 @@ declare global {
}; };
viewer: { viewer: {
open: (anchorPhoto: IPhoto, rows: IRow[]) => Promise<void>; open: (photo: IPhoto) => void;
openDynamic: (anchorPhoto: IPhoto, rows: IRow[]) => Promise<void>;
openStatic(photo: IPhoto, list: IPhoto[], thumbSize?: 256 | 512): Promise<void>; openStatic(photo: IPhoto, list: IPhoto[], thumbSize?: 256 | 512): Promise<void>;
close: () => void; close: () => void;
isOpen: boolean; isOpen: boolean;

View File

@ -0,0 +1,166 @@
import type { IPhoto } from '../../types';
/** Viewer Fragment */
type FragmentTypeViewer = 'v';
/** All types of fragmemts */
type FragmentType = FragmentTypeViewer;
/** Data structure to encode to fragment */
type FragmentObj = {
type: FragmentType;
args: string[];
index: number;
};
/**
* Decode fragments from string.
* @param hash Hash string
*/
function decodeFragment(hash: string): FragmentObj[] {
return hash
.substring(1) // remove # at start
.split('&') // get all parts
.filter((frag) => frag) // remove empty parts
.map((frag, i, arr) => {
const values = frag?.split('/');
return {
type: (values?.[0] ?? 'u') as FragmentType,
args: values?.slice(1) ?? [],
index: arr.length - i - 1,
};
});
}
/**
* Encode fragments to string.
* @param fragments Fragments to encode
*/
function encodeFragment(fragments: FragmentObj[]): string {
if (!fragments.length) return '';
return '#' + fragments.map((frag) => [frag.type, ...frag.args].join('/')).join('&');
}
/**
* Cache for route fragments
*/
const cache = {
hash: String(),
list: [] as FragmentObj[],
};
export const fragment = {
/**
* Get list of all fragments in route.
* @returns List of fragments
*/
get list(): FragmentObj[] {
if (cache.hash !== _m.route.hash) {
cache.hash = _m.route.hash;
cache.list = decodeFragment(cache.hash ?? String());
}
return cache.list;
},
/**
* Check if route has this fragment type.
* @param type Fragment identifier
*/
get(type: FragmentType) {
return this.list.find((frag) => frag.type === type);
},
/**
* Add fragment to route.
* @param frag Fragment to add to route
*/
push(frag: FragmentObj) {
const list = this.list;
// Get the top fragment
const top = list[list.length - 1];
// Check if we are already on this fragment
if (top?.type === frag.type) {
// Replace the arguments
top.args = frag.args;
const hash = encodeFragment(list);
// Avoid redundant route changes
if (hash === _m.route.hash) return;
// Replace the route with the new fragment
_m.router.replace({
path: _m.route.path,
query: _m.route.query,
hash: hash,
});
return;
}
// If the fragment is already in the list,
// we can't touch it. This should never happen.
if (list.find((f) => f.type === frag.type)) {
console.error('[BUG] Fragment already in route', frag.type);
}
// Add fragment to route
list.push(frag);
_m.router.push({
path: _m.route.path,
query: _m.route.query,
hash: encodeFragment(list),
});
},
/**
* Remove the top fragment from route.
* @param type Fragment identifier
*/
pop(type: FragmentType) {
// Get the index of this fragment from the end
const frag = this.get(type);
if (!frag) return;
// Go back in history
_m.router.go(-frag.index - 1);
// Check if the fragment still exists
// In that case, replace the route to remove the fragment
const sfrag = this.get(type);
if (sfrag) {
_m.router.replace({
path: _m.route.path,
query: _m.route.query,
hash: encodeFragment(this.list.slice(0, -sfrag.index - 1)),
});
}
},
get viewer() {
const frag = this.get('v');
const typed = {
open: !!frag,
type: frag?.type ?? 'v',
args: (frag?.args ?? ['0', '']) as [string, string],
index: frag?.index ?? -1,
get dayid() {
return parseInt(this.args[0]);
},
set dayid(dayid: number) {
this.args[0] = String(dayid);
},
get key() {
return this.args[1];
},
set key(key: string) {
this.args[1] = key;
},
};
return typed;
},
};

View File

@ -190,24 +190,6 @@ export function setupLivePhotoHooks(video: HTMLVideoElement) {
}; };
} }
/**
* Get route hash for viewer for photo
*/
export function getViewerHash(photo: IPhoto) {
return `#v/${photo.dayid}/${photo.key}`;
}
/**
* Get route for viewer for photo
*/
export function getViewerRoute(photo: IPhoto) {
return {
path: _m.route.path,
query: _m.route.query,
hash: getViewerHash(photo),
};
}
/** /**
* Choose a folder using the NC file picker * Choose a folder using the NC file picker
* *

View File

@ -5,3 +5,4 @@ export * from './date';
export * from './helpers'; export * from './helpers';
export * from './dialog'; export * from './dialog';
export * from './event-bus'; export * from './event-bus';
export * from './fragment';