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() {
|
get route() {
|
||||||
return router.currentRoute;
|
return router.currentRoute;
|
||||||
},
|
},
|
||||||
|
router: router,
|
||||||
routes: routes,
|
routes: routes,
|
||||||
|
|
||||||
modals: {} as any,
|
modals: {} as any,
|
||||||
|
|
|
@ -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);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
* Choose a folder using the NC file picker
|
||||||
*
|
*
|
||||||
|
|
|
@ -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';
|
||||||
|
|
Loading…
Reference in New Issue