refactor(fragment): support multiple frags
Signed-off-by: Varun Patil <radialapps@gmail.com>pull/888/head
parent
182caed840
commit
557e87ca3c
|
@ -28,6 +28,7 @@ globalThis._m = {
|
|||
get route() {
|
||||
return router.currentRoute;
|
||||
},
|
||||
router: router,
|
||||
routes: routes,
|
||||
|
||||
modals: {} as any,
|
||||
|
|
|
@ -867,7 +867,7 @@ export default defineComponent({
|
|||
/** Open viewer with given photo */
|
||||
openViewer(photo: IPhoto) {
|
||||
nativex.playTouchSound();
|
||||
this.$router.push(utils.getViewerRoute(photo));
|
||||
_m.viewer.open(photo);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -269,14 +269,9 @@ export default defineComponent({
|
|||
await this.$nextTick();
|
||||
|
||||
// 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
|
||||
const parts = to.hash.split('/');
|
||||
if (parts.length !== 3) return;
|
||||
|
||||
// Get params
|
||||
const dayid = parseInt(parts[1]);
|
||||
const key = parts[2];
|
||||
const { dayid, key } = utils.fragment.viewer;
|
||||
if (isNaN(dayid) || !key) return;
|
||||
|
||||
// Get day
|
||||
|
@ -299,9 +294,9 @@ export default defineComponent({
|
|||
}
|
||||
}
|
||||
|
||||
_m.viewer.open(photo, this.list);
|
||||
} else if (!to.hash?.startsWith('#v') && _m.viewer.isOpen) {
|
||||
// Close viewer
|
||||
_m.viewer.openDynamic(photo, this.list);
|
||||
} else if (!utils.fragment.viewer.open && _m.viewer.isOpen) {
|
||||
// No viewer fragment but viewer is open
|
||||
_m.viewer.close();
|
||||
}
|
||||
},
|
||||
|
@ -693,7 +688,7 @@ export default defineComponent({
|
|||
data = await dav.getOnThisDayData();
|
||||
} else if (dav.isSingleItem()) {
|
||||
data = await dav.getSingleItemData();
|
||||
this.$router.replace(utils.getViewerRoute(data[0]!.detail![0]));
|
||||
_m.viewer.open(data[0]!.detail![0]);
|
||||
} else {
|
||||
// Try the cache
|
||||
if (!noCache) {
|
||||
|
|
|
@ -298,7 +298,7 @@ export default defineComponent({
|
|||
// At high zoom levels, open the photo
|
||||
if (this.zoom >= 12 && cluster.preview) {
|
||||
cluster.preview.key = cluster.preview.fileid.toString();
|
||||
this.$router.push(utils.getViewerRoute(cluster.preview));
|
||||
_m.viewer.open(cluster.preview);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -287,7 +287,8 @@ export default defineComponent({
|
|||
// The viewer is a singleton
|
||||
const self = this;
|
||||
_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,
|
||||
close: this.close.bind(this) as typeof this.close,
|
||||
get isOpen() {
|
||||
|
@ -553,7 +554,7 @@ export default defineComponent({
|
|||
this.fullyOpened = false;
|
||||
this.setUiVisible(false);
|
||||
this.hideSidebar();
|
||||
this.setRouteHash(undefined);
|
||||
this.setFragment(null);
|
||||
this.updateTitle(undefined);
|
||||
nativex.setTheme(); // reset
|
||||
document.body.classList.remove(BODY_VIEWER_VIDEO);
|
||||
|
@ -581,7 +582,7 @@ export default defineComponent({
|
|||
this.photoswipe.on('slideActivate', (e) => {
|
||||
this.currIndex = this.photoswipe!.currIndex;
|
||||
const photo = e.slide?.data?.photo;
|
||||
this.setRouteHash(photo);
|
||||
this.setFragment(photo);
|
||||
this.updateTitle(photo);
|
||||
});
|
||||
|
||||
|
@ -636,7 +637,7 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
/** Open using start photo and rows list */
|
||||
async open(anchorPhoto: IPhoto, rows: IRow[]) {
|
||||
async openDynamic(anchorPhoto: IPhoto, rows: IRow[]) {
|
||||
const detail = anchorPhoto.d?.detail;
|
||||
if (!detail) {
|
||||
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 */
|
||||
setRouteHash(photo: IPhoto | undefined) {
|
||||
if (!photo) {
|
||||
if (!this.isOpen && this.$route.hash?.startsWith('#v')) {
|
||||
this.$router.back();
|
||||
|
||||
// Ensure this does not have the hash, otherwise replace it
|
||||
if (this.$route.hash?.startsWith('#v')) {
|
||||
this.$router.replace({
|
||||
hash: '',
|
||||
query: this.$route.query,
|
||||
});
|
||||
}
|
||||
}
|
||||
return;
|
||||
setFragment(photo: IPhoto | null) {
|
||||
if (photo) {
|
||||
const frag = utils.fragment.viewer;
|
||||
frag.dayid = photo.dayid;
|
||||
frag.key = photo.key!;
|
||||
return utils.fragment.push(frag);
|
||||
}
|
||||
const hash = photo ? utils.getViewerHash(photo) : '';
|
||||
const route = {
|
||||
path: this.$route.path,
|
||||
query: this.$route.query,
|
||||
hash,
|
||||
};
|
||||
if (hash !== this.$route.hash) {
|
||||
if (this.$route.hash) {
|
||||
this.$router.replace(route);
|
||||
} else {
|
||||
this.$router.push(route);
|
||||
}
|
||||
|
||||
if (!this.isOpen) {
|
||||
return utils.fragment.pop('v');
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { Route } from 'vue-router';
|
||||
import Router, { Route } from 'vue-router';
|
||||
import type { ComponentPublicInstance } from 'vue';
|
||||
|
||||
import type { translate, translatePlural } from '@nextcloud/l10n';
|
||||
|
@ -32,6 +32,7 @@ declare global {
|
|||
var _m: {
|
||||
mode: 'admin' | 'user';
|
||||
route: Route;
|
||||
router: Router;
|
||||
routes: typeof routes;
|
||||
|
||||
modals: {
|
||||
|
@ -52,7 +53,8 @@ declare global {
|
|||
};
|
||||
|
||||
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>;
|
||||
close: () => void;
|
||||
isOpen: boolean;
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
};
|
|
@ -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
|
||||
*
|
||||
|
|
|
@ -5,3 +5,4 @@ export * from './date';
|
|||
export * from './helpers';
|
||||
export * from './dialog';
|
||||
export * from './event-bus';
|
||||
export * from './fragment';
|
||||
|
|
Loading…
Reference in New Issue