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() {
return router.currentRoute;
},
router: router,
routes: routes,
modals: {} as any,

View File

@ -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);
},
},
});

View File

@ -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) {

View File

@ -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;
}

View File

@ -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();
setFragment(photo: IPhoto | null) {
if (photo) {
const frag = utils.fragment.viewer;
frag.dayid = photo.dayid;
frag.key = photo.key!;
return utils.fragment.push(frag);
}
// 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;
}
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');
}
},

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 { 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;

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
*

View File

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