Skip to content

Commit

Permalink
feat(app): integrate with view transitions to keep state
Browse files Browse the repository at this point in the history
  • Loading branch information
yjl9903 committed Aug 11, 2023
1 parent 9388966 commit 46aa773
Show file tree
Hide file tree
Showing 7 changed files with 339 additions and 227 deletions.
3 changes: 3 additions & 0 deletions packages/app/astro.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,8 @@ export default defineConfig({
}),
vite: {
plugins: [Info(), TsconfigPaths()]
},
experimental: {
viewTransitions: true
}
});
48 changes: 3 additions & 45 deletions packages/app/src/components/ResourceTable.astro
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
---
import type { Resource, ResourceType } from 'animegarden';
import type { Resource } from 'animegarden';
import { formatInTimeZone } from 'date-fns-tz';
import { DisplayType, DisplayTypeColor } from '../constant';
import Tag from './Tag.astro';
export interface Props {
Expand All @@ -13,50 +15,6 @@ const { resources } = Astro.props;
const search = Astro.url.searchParams;
const DisplayType: Record<ResourceType, string> = {
動畫: '动画',
季度全集: '季度全集',
音樂: '音乐',
動漫音樂: '动漫音乐',
同人音樂: '同人音乐',
流行音樂: '流行音乐',
日劇: '日剧',
RAW: 'RAW',
其他: '其他',
漫畫: '漫画',
港台原版: '港台原版',
日文原版: '日文原版',
遊戲: '游戏',
電腦遊戲: '电脑游戏',
電視遊戲: '主机游戏',
掌機遊戲: '掌机游戏',
網絡遊戲: '网络游戏 ',
遊戲周邊: '游戏周边',
特攝: '特摄'
};
const DisplayTypeColor: Record<ResourceType, string> = {
動畫: 'text-red-600',
季度全集: 'text-red-700',
漫畫: 'text-green-600',
港台原版: 'text-green-600',
日文原版: 'text-green-600',
音樂: 'text-purple-600',
動漫音樂: 'text-purple-600',
同人音樂: 'text-purple-600',
流行音樂: 'text-purple-600',
日劇: 'text-blue-600',
RAW: '',
遊戲: '',
電腦遊戲: '',
電視遊戲: '',
掌機遊戲: '',
網絡遊戲: '',
遊戲周邊: '',
特攝: 'text-rose-600',
其他: 'text-base-800'
};
function getMirrorHref(href: string) {
return `/resource/${href.split('/').at(-1)}`;
}
Expand Down
105 changes: 50 additions & 55 deletions packages/app/src/components/Search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import { useStore } from '@nanostores/react';
import { findFansub, stringifySearchURL } from 'animegarden';
import { useCallback, useEffect, useRef, useState } from 'react';

import '../styles/cmdk.css';

import { fetchResources } from '../fetch';
import { fansubs, types } from '../constant';
import { histories, loading } from '../state';

import { useActiveElement, useSessionStorage } from './hooks';

const SearchInputKey = 'search:input';

{
document.addEventListener('keypress', (ev) => {
if (ev.key === 's' || ev.key === '/') {
Expand All @@ -20,58 +22,43 @@ import { histories, loading } from '../state';
ev.stopPropagation();
}
});

document.addEventListener('astro:beforeload', () => {});
}

const DMHY_RE = /(?:https:\/\/share.dmhy.org\/topics\/view\/)?(\d+_[a-zA-Z0-9_\-]+\.html)/;

const useActiveElement = () => {
const [listenersReady, setListenersReady] = useState(false);
const [activeElement, setActiveElement] = useState(document.activeElement);

useEffect(() => {
const onFocus = (event: FocusEvent) => setActiveElement(event.target as any);
const onBlur = (event: FocusEvent) => setActiveElement(null);

window.addEventListener('focus', onFocus, true);
window.addEventListener('blur', onBlur, true);

setListenersReady(true);

return () => {
window.removeEventListener('focus', onFocus);
window.removeEventListener('blur', onBlur);
};
}, []);

return {
active: activeElement,
ready: listenersReady
};
};

const initialized = {
url: location as Location | undefined
};

export default function Search() {
const ref = useRef<HTMLDivElement | null>(null);
const inputRef = useRef<HTMLInputElement | null>(null);
const { active } = useActiveElement();

const history = useStore(histories);
const [input, setInput] = useState('');
const [input, setInput] = useSessionStorage(SearchInputKey, '');
const [search, setSearch] = useState('');

// Init search input
if (initialized.url !== undefined) {
const searchParams = new URLSearchParams(initialized.url.search);
const cmd = searchParams.get('cmd');
if (cmd !== undefined && typeof cmd === 'string') {
setInput(cmd);
setSearch(cmd);
}
initialized.url = undefined;
}
useEffect(() => {
const fn = () => {
try {
const input = window.sessionStorage.getItem(SearchInputKey);
if (input) {
const target = JSON.parse(input);
if (typeof target === 'string' && target) {
const current = window.location.pathname + window.location.search;
console.log(current, target, resolveSearchURL(target));
if (current !== resolveSearchURL(target)) {
setInput('');
}
}
}
} catch {}
};

document.addEventListener('astro:load', fn);
return () => {
document.removeEventListener('astro:load', fn);
};
});

const setDebounceSearch = debounce((value: string) => {
if (value !== search) {
Expand Down Expand Up @@ -128,12 +115,16 @@ export default function Search() {
(text?: string) => {
const target = text ?? input;
if (target) {
// Filter old history item which is the substring of the current input
const oldHistories = histories.get().filter((o) => !target.includes(o));
// Remove duplicate items
const newHistories = [...new Set([target, ...oldHistories])].slice(0, 10);
// Set histories
histories.set(newHistories);
setInput(target);

{
// Filter old history item which is the substring of the current input
const oldHistories = histories.get().filter((o) => !target.includes(o));
// Remove duplicate items
const newHistories = [...new Set([target, ...oldHistories])].slice(0, 10);
// Set histories
histories.set(newHistories);
}

stopFetch();
goToSearch(target);
Expand Down Expand Up @@ -358,26 +349,30 @@ function parseSearch(search: string) {
};
}

function goToSearch(search: string) {
function resolveSearchURL(search: string) {
if (search.startsWith(location.origin)) {
goTo(search.slice(location.origin.length));
return search.slice(location.origin.length);
} else if (search.startsWith(location.host)) {
goTo(search.slice(location.host.length));
return search.slice(location.host.length);
} else {
const match = DMHY_RE.exec(search);
if (match) {
goTo(`/resource/${match[1]}`);
return `/resource/${match[1]}`;
} else {
const url = stringifySearchURL(location.origin, parseSearch(search));
url.searchParams.set('cmd', search);
goTo(`${url.pathname}${url.search}`);
return `${url.pathname}${url.search}`;
}
}
}

function goToSearch(search: string) {
return goTo(resolveSearchURL(search));
}

function goTo(href: string) {
loading.set(true);
window.location.href = href;
// window.location.href = href;
window.open(href, '_self');
}

function debounce<T extends (...args: any[]) => void>(fn: T, time = 1000): T {
Expand Down
66 changes: 66 additions & 0 deletions packages/app/src/components/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { useEffect, useState } from 'react';

export const useActiveElement = () => {
const [listenersReady, setListenersReady] = useState(false);
const [activeElement, setActiveElement] = useState(document.activeElement);

useEffect(() => {
const onFocus = (event: FocusEvent) => setActiveElement(event.target as any);
const onBlur = (event: FocusEvent) => setActiveElement(null);

window.addEventListener('focus', onFocus, true);
window.addEventListener('blur', onBlur, true);

setListenersReady(true);

return () => {
window.removeEventListener('focus', onFocus);
window.removeEventListener('blur', onBlur);
};
}, []);

return {
active: activeElement,
ready: listenersReady
};
};

/**
* Copied from https://github.com/streamich/react-use/blob/master/src/useSessionStorage.ts
*/
export const useSessionStorage = <T>(
key: string,
initialValue?: T,
raw?: boolean
): [T, (value: T) => void] => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const [state, setState] = useState<T>(() => {
try {
const sessionStorageValue = sessionStorage.getItem(key);
if (typeof sessionStorageValue !== 'string') {
sessionStorage.setItem(key, raw ? String(initialValue) : JSON.stringify(initialValue));
return initialValue;
} else {
return raw ? sessionStorageValue : (JSON.parse(sessionStorageValue || 'null') as any);
}
} catch {
// If user is in private mode or has storage restriction
// sessionStorage can throw. JSON.parse and JSON.stringify
// can throw, too.
return initialValue;
}
});

// eslint-disable-next-line react-hooks/rules-of-hooks
useEffect(() => {
try {
const serializedState = raw ? String(state) : JSON.stringify(state);
sessionStorage.setItem(key, serializedState);
} catch {
// If user is in private mode or has storage restriction
// sessionStorage can throw. Also JSON.stringify can throw.
}
});

return [state, setState];
};
46 changes: 46 additions & 0 deletions packages/app/src/constant.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { ResourceType } from 'animegarden';

export const WORKER_BASE = `animegarden.yjl9903.workers.dev`;

export const types = [
Expand Down Expand Up @@ -44,6 +46,50 @@ export const QueryType: Record<string, string> = {
特摄: '特攝'
};

export const DisplayType: Record<ResourceType, string> = {
動畫: '动画',
季度全集: '季度全集',
音樂: '音乐',
動漫音樂: '动漫音乐',
同人音樂: '同人音乐',
流行音樂: '流行音乐',
日劇: '日剧',
RAW: 'RAW',
其他: '其他',
漫畫: '漫画',
港台原版: '港台原版',
日文原版: '日文原版',
遊戲: '游戏',
電腦遊戲: '电脑游戏',
電視遊戲: '主机游戏',
掌機遊戲: '掌机游戏',
網絡遊戲: '网络游戏 ',
遊戲周邊: '游戏周边',
特攝: '特摄'
};

export const DisplayTypeColor: Record<ResourceType, string> = {
動畫: 'text-red-600',
季度全集: 'text-red-700',
漫畫: 'text-green-600',
港台原版: 'text-green-600',
日文原版: 'text-green-600',
音樂: 'text-purple-600',
動漫音樂: 'text-purple-600',
同人音樂: 'text-purple-600',
流行音樂: 'text-purple-600',
日劇: 'text-blue-600',
RAW: '',
遊戲: '',
電腦遊戲: '',
電視遊戲: '',
掌機遊戲: '',
網絡遊戲: '',
遊戲周邊: '',
特攝: 'text-rose-600',
其他: 'text-base-800'
};

export const fansubs = [
{ id: 619, name: '桜都字幕组' },
{ id: 833, name: '北宇治字幕组' },
Expand Down
Loading

0 comments on commit 46aa773

Please sign in to comment.