feat: refresh shared chat and server workflows
This commit is contained in:
@@ -79,6 +79,12 @@
|
||||
rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.apps-library__card--baseball-ticket-bay {
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 128, 82, 0.3), rgba(78, 132, 255, 0.14)),
|
||||
rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.apps-library__card--beat {
|
||||
background:
|
||||
linear-gradient(180deg, rgba(127, 114, 255, 0.18), rgba(255, 255, 255, 0.04)),
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Tag } from 'antd';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useLocation, useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import './AppsLibraryView.css';
|
||||
import { BaseballTicketBayPlayAppView } from '../baseball-ticket-bay/BaseballTicketBayPlayAppView';
|
||||
import { EReaderAppView } from '../e-reader/EReaderAppView';
|
||||
import { PhotoPrismAppView } from '../photoprism/PhotoPrismAppView';
|
||||
import { PhotoPuzzleAppView } from '../photo-puzzle/PhotoPuzzleAppView';
|
||||
@@ -72,6 +73,10 @@ export function AppsLibraryView() {
|
||||
return <PhotoPrismAppView onBack={closeApp} launchContext={launchContext} />;
|
||||
}
|
||||
|
||||
if (activeAppEntry?.id === 'baseball-ticket-bay') {
|
||||
return <BaseballTicketBayPlayAppView onBack={closeApp} launchContext={launchContext} />;
|
||||
}
|
||||
|
||||
if (activeAppEntry?.id === 'e-reader') {
|
||||
return <EReaderAppView onBack={closeApp} launchContext={launchContext} />;
|
||||
}
|
||||
@@ -110,6 +115,8 @@ export function AppsLibraryView() {
|
||||
data-testid={
|
||||
entry.id === 'e-reader'
|
||||
? 'apps-library-open-e-reader'
|
||||
: entry.id === 'baseball-ticket-bay'
|
||||
? 'apps-library-open-baseball-ticket-bay'
|
||||
: entry.id === 'photoprism'
|
||||
? 'apps-library-open-photoprism'
|
||||
: entry.id === 'photo-puzzle'
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
AppstoreOutlined,
|
||||
BellOutlined,
|
||||
BookOutlined,
|
||||
FileImageOutlined,
|
||||
FireOutlined,
|
||||
@@ -21,12 +22,25 @@ export type PlayAppEntry = {
|
||||
statusLabel: string;
|
||||
isReady: boolean;
|
||||
icon: ReactNode;
|
||||
usagePriority?: number;
|
||||
supportedEnvironments?: PlayAppEnvironment[];
|
||||
searchKeywords?: string[];
|
||||
searchDescription?: string;
|
||||
};
|
||||
|
||||
export const APP_LIBRARY_ENTRIES: PlayAppEntry[] = [
|
||||
{
|
||||
id: 'baseball-ticket-bay',
|
||||
name: '야구-티켓베이',
|
||||
accentClassName: 'apps-library__card--baseball-ticket-bay',
|
||||
statusLabel: '알림',
|
||||
isReady: true,
|
||||
icon: <BellOutlined />,
|
||||
usagePriority: 100,
|
||||
supportedEnvironments: ['preview', 'test'],
|
||||
searchKeywords: ['야구', '티켓베이', 'ticketbay', '야구 티켓', '웹푸시', '알림'],
|
||||
searchDescription: '팀, 구역, 통로, 가격 조건으로 야구 티켓 알림 조건을 저장하고 테스트 푸시를 보냅니다.',
|
||||
},
|
||||
{
|
||||
id: 'e-reader',
|
||||
name: 'E-Reader',
|
||||
@@ -34,6 +48,7 @@ export const APP_LIBRARY_ENTRIES: PlayAppEntry[] = [
|
||||
statusLabel: '읽기',
|
||||
isReady: true,
|
||||
icon: <BookOutlined />,
|
||||
usagePriority: 80,
|
||||
supportedEnvironments: ['preview', 'test'],
|
||||
searchKeywords: ['e-reader', 'reader', 'ebook', 'article', 'web article', '기사', '전자책', '리더'],
|
||||
searchDescription: 'Apps 보관함에서 인터넷 기사와 웹 콘텐츠를 전자책처럼 넘겨 읽습니다.',
|
||||
@@ -45,6 +60,7 @@ export const APP_LIBRARY_ENTRIES: PlayAppEntry[] = [
|
||||
statusLabel: '연결',
|
||||
isReady: true,
|
||||
icon: <FileImageOutlined />,
|
||||
usagePriority: 70,
|
||||
supportedEnvironments: ['preview', 'test'],
|
||||
searchKeywords: ['photoprism', 'photo', 'album', 'gallery', '사진 앨범'],
|
||||
searchDescription: 'Apps 보관함에서 PhotoPrism 앨범과 사진을 단독 앱 형태로 엽니다.',
|
||||
@@ -56,6 +72,7 @@ export const APP_LIBRARY_ENTRIES: PlayAppEntry[] = [
|
||||
statusLabel: '실행',
|
||||
isReady: true,
|
||||
icon: <PictureOutlined />,
|
||||
usagePriority: 60,
|
||||
supportedEnvironments: ['preview', 'test'],
|
||||
searchKeywords: ['photo puzzle', '퍼즐', '사진', '슬라이드 퍼즐'],
|
||||
searchDescription: 'Apps 보관함에서 사진 퍼즐 게임을 바로 실행합니다.',
|
||||
@@ -67,6 +84,7 @@ export const APP_LIBRARY_ENTRIES: PlayAppEntry[] = [
|
||||
statusLabel: '신규',
|
||||
isReady: true,
|
||||
icon: <ThunderboltOutlined />,
|
||||
usagePriority: 50,
|
||||
supportedEnvironments: ['preview', 'test'],
|
||||
searchKeywords: ['the quest', 'rpg', 'mobile rpg', 'phaser', 'quest', 'rpg game', '모바일 rpg'],
|
||||
searchDescription: 'Apps 보관함에서 한국형 모바일 RPG 데모 The Quest를 실행합니다.',
|
||||
@@ -78,6 +96,7 @@ export const APP_LIBRARY_ENTRIES: PlayAppEntry[] = [
|
||||
statusLabel: '실행',
|
||||
isReady: true,
|
||||
icon: <FundProjectionScreenOutlined />,
|
||||
usagePriority: 40,
|
||||
supportedEnvironments: ['preview', 'test'],
|
||||
searchKeywords: ['테트리스', 'block', 'arcade', '블록 게임'],
|
||||
searchDescription: 'Apps 보관함에서 테트리스 게임을 바로 실행합니다.',
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
253
src/views/play/apps/baseball-ticket-bay/baseballTicketBayApi.ts
Normal file
253
src/views/play/apps/baseball-ticket-bay/baseballTicketBayApi.ts
Normal file
@@ -0,0 +1,253 @@
|
||||
import { appendClientIdHeader } from '../../../../app/main/clientIdentity';
|
||||
import { getRegisteredAccessToken } from '../../../../app/main/tokenAccess';
|
||||
|
||||
const WORK_SERVER_TIMEOUT_MS = 15_000;
|
||||
const BASEBALL_TICKET_BAY_RUN_TIMEOUT_MS = 180_000;
|
||||
|
||||
export type BaseballTicketBayTimeWindow = {
|
||||
id: string;
|
||||
start: string;
|
||||
end: string;
|
||||
};
|
||||
|
||||
export type BaseballTicketBayAlertItem = {
|
||||
id: string;
|
||||
title: string;
|
||||
eventDate: string;
|
||||
team: string;
|
||||
zone: string;
|
||||
aisleSide: string;
|
||||
seatDirections: string[];
|
||||
maxPrice: number | null;
|
||||
seatCount: number;
|
||||
batchIntervalMinutes: number;
|
||||
sameProductAlertEnabled: boolean;
|
||||
sameProductNotifyOnce: boolean;
|
||||
active: boolean;
|
||||
timeWindows: BaseballTicketBayTimeWindow[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
lastRunAt: string | null;
|
||||
lastMatchAt: string | null;
|
||||
};
|
||||
|
||||
export type BaseballTicketBayMatchResult = {
|
||||
productId: number;
|
||||
displayNumber: string;
|
||||
saleUrl: string;
|
||||
title: string;
|
||||
eventDateTime: string;
|
||||
categoryName: string;
|
||||
teamName: string;
|
||||
area: string;
|
||||
rowLabel: string;
|
||||
row: string;
|
||||
opponentOrFloor: string;
|
||||
grade: string;
|
||||
addInfo: string;
|
||||
seatCount: number | null;
|
||||
together: boolean;
|
||||
price: number | null;
|
||||
totalPrice: number | null;
|
||||
transactionType: string;
|
||||
sellerPost: string;
|
||||
productRemarks: string[];
|
||||
seatRemarks: string[];
|
||||
seatMapImageUrl: string | null;
|
||||
photoUrls: string[];
|
||||
sellerPhotoCount: number;
|
||||
createdAt: string;
|
||||
safeTrade: boolean;
|
||||
pinTrade: boolean;
|
||||
deliveryTrade: boolean;
|
||||
fieldTrade: boolean;
|
||||
etcTrade: boolean;
|
||||
};
|
||||
|
||||
export type BaseballTicketBayRunPayload = {
|
||||
keyword: string;
|
||||
scannedCategoryCount: number;
|
||||
scannedItemTotalCount?: number;
|
||||
scannedCategories: Array<{
|
||||
categoryId: number;
|
||||
categoryName: string;
|
||||
pageCount: number;
|
||||
scannedItemCount: number;
|
||||
}>;
|
||||
rejectionSummary?: Array<{
|
||||
reason: string;
|
||||
label: string;
|
||||
count: number;
|
||||
samples: string[];
|
||||
}>;
|
||||
results: BaseballTicketBayMatchResult[];
|
||||
};
|
||||
|
||||
export type BaseballTicketBayAlertLogItem = {
|
||||
id: string;
|
||||
alertId: string | null;
|
||||
alertTitle: string;
|
||||
action: 'create' | 'run' | 'pause' | 'resume' | 'delete' | 'push';
|
||||
status: 'info' | 'success' | 'warning' | 'error';
|
||||
message: string;
|
||||
detail: string;
|
||||
createdAt: string;
|
||||
payload?: BaseballTicketBayRunPayload | null;
|
||||
};
|
||||
|
||||
type BaseballTicketBayAlertMutation = Omit<
|
||||
BaseballTicketBayAlertItem,
|
||||
'id' | 'createdAt' | 'updatedAt' | 'lastRunAt' | 'lastMatchAt'
|
||||
>;
|
||||
|
||||
type BaseballTicketBayAlertsResponse = {
|
||||
ok: boolean;
|
||||
items: BaseballTicketBayAlertItem[];
|
||||
};
|
||||
|
||||
type BaseballTicketBayLogsResponse = {
|
||||
ok: boolean;
|
||||
items: BaseballTicketBayAlertLogItem[];
|
||||
};
|
||||
|
||||
type BaseballTicketBayAlertResponse = {
|
||||
ok: boolean;
|
||||
item: BaseballTicketBayAlertItem;
|
||||
};
|
||||
|
||||
type BaseballTicketBayRunResponse = {
|
||||
ok: boolean;
|
||||
alert: BaseballTicketBayAlertItem;
|
||||
matches: BaseballTicketBayMatchResult[];
|
||||
notifiedMatches: BaseballTicketBayMatchResult[];
|
||||
log: BaseballTicketBayAlertLogItem;
|
||||
};
|
||||
|
||||
function resolveApiBaseUrl() {
|
||||
if (import.meta.env.VITE_WORK_SERVER_URL) {
|
||||
return import.meta.env.VITE_WORK_SERVER_URL;
|
||||
}
|
||||
|
||||
return '/api';
|
||||
}
|
||||
|
||||
const API_BASE_URL = resolveApiBaseUrl();
|
||||
|
||||
function buildHeaders(headersInit?: HeadersInit, hasJsonBody = false) {
|
||||
const headers = appendClientIdHeader(headersInit);
|
||||
const token = getRegisteredAccessToken();
|
||||
|
||||
if (hasJsonBody && !headers.has('Content-Type')) {
|
||||
headers.set('Content-Type', 'application/json');
|
||||
}
|
||||
|
||||
if (token && !headers.has('X-Access-Token')) {
|
||||
headers.set('X-Access-Token', token);
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
async function request<T>(path: string, init?: (RequestInit & { timeoutMs?: number })) {
|
||||
const controller = new AbortController();
|
||||
const timeoutMs =
|
||||
typeof init?.timeoutMs === 'number' && Number.isFinite(init.timeoutMs)
|
||||
? Math.max(1_000, init.timeoutMs)
|
||||
: WORK_SERVER_TIMEOUT_MS;
|
||||
const timeoutId = window.setTimeout(() => controller.abort(), timeoutMs);
|
||||
const hasBody = init?.body != null;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}${path}`, {
|
||||
...init,
|
||||
headers: buildHeaders(init?.headers, hasBody),
|
||||
signal: controller.signal,
|
||||
cache: init?.cache ?? 'no-store',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(text) as { message?: string };
|
||||
throw new Error(parsed.message || '야구 티켓베이 요청에 실패했습니다.');
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message !== text) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new Error(text || '야구 티켓베이 요청에 실패했습니다.');
|
||||
}
|
||||
}
|
||||
|
||||
return (await response.json()) as T;
|
||||
} catch (error) {
|
||||
if (error instanceof DOMException && error.name === 'AbortError') {
|
||||
throw new Error('야구 티켓베이 응답이 지연됩니다.');
|
||||
}
|
||||
|
||||
throw error;
|
||||
} finally {
|
||||
window.clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchBaseballTicketBayAlerts() {
|
||||
const response = await request<BaseballTicketBayAlertsResponse>('/baseball-ticket-bay/alerts');
|
||||
return response.items;
|
||||
}
|
||||
|
||||
export async function fetchBaseballTicketBayLogs(alertId?: string | null) {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (alertId) {
|
||||
params.set('alertId', alertId);
|
||||
}
|
||||
|
||||
const path = params.size > 0 ? `/baseball-ticket-bay/logs?${params.toString()}` : '/baseball-ticket-bay/logs';
|
||||
const response = await request<BaseballTicketBayLogsResponse>(path);
|
||||
return response.items;
|
||||
}
|
||||
|
||||
export async function deleteBaseballTicketBayLog(logId: string) {
|
||||
await request<{ ok: boolean; item: BaseballTicketBayAlertLogItem | null }>(
|
||||
`/baseball-ticket-bay/logs/${encodeURIComponent(logId)}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export async function createBaseballTicketBayAlert(payload: BaseballTicketBayAlertMutation) {
|
||||
const response = await request<BaseballTicketBayAlertResponse>('/baseball-ticket-bay/alerts', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
return response.item;
|
||||
}
|
||||
|
||||
export async function updateBaseballTicketBayAlert(alertId: string, payload: Partial<BaseballTicketBayAlertMutation>) {
|
||||
const response = await request<BaseballTicketBayAlertResponse>(`/baseball-ticket-bay/alerts/${encodeURIComponent(alertId)}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
return response.item;
|
||||
}
|
||||
|
||||
export async function deleteBaseballTicketBayAlert(alertId: string) {
|
||||
await request<{ ok: boolean; item: BaseballTicketBayAlertItem | null }>(
|
||||
`/baseball-ticket-bay/alerts/${encodeURIComponent(alertId)}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export async function runBaseballTicketBayAlert(alertId: string) {
|
||||
return request<BaseballTicketBayRunResponse>(`/baseball-ticket-bay/alerts/${encodeURIComponent(alertId)}/run`, {
|
||||
method: 'POST',
|
||||
timeoutMs: BASEBALL_TICKET_BAY_RUN_TIMEOUT_MS,
|
||||
});
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
listReaderLibraryArticles,
|
||||
searchReaderNews,
|
||||
saveReaderLibraryArticle,
|
||||
setEReaderShareTokenOverride,
|
||||
type EReaderLibraryArticle,
|
||||
type EReaderNewsArticle,
|
||||
type EReaderNewsSearchParams,
|
||||
@@ -33,6 +34,7 @@ import './EReaderAppView.css';
|
||||
type EReaderAppViewProps = {
|
||||
onBack: () => void;
|
||||
launchContext?: 'direct' | 'embedded';
|
||||
shareToken?: string | null;
|
||||
};
|
||||
|
||||
type ReaderTheme = 'mist' | 'ocean' | 'night' | 'sepia' | 'forest' | 'graphite' | 'rose' | 'dawn';
|
||||
@@ -994,6 +996,18 @@ function getReaderLaunchUrl() {
|
||||
return new URL(getReaderLaunchPath(), window.location.origin).toString();
|
||||
}
|
||||
|
||||
function normalizeShareToken(value: string | null | undefined) {
|
||||
return value?.trim() ?? '';
|
||||
}
|
||||
|
||||
function readShareTokenFromUrl() {
|
||||
if (typeof window === 'undefined') {
|
||||
return '';
|
||||
}
|
||||
|
||||
return normalizeShareToken(new URLSearchParams(window.location.search).get('shareToken'));
|
||||
}
|
||||
|
||||
function getInstallGuideMessage() {
|
||||
if (typeof navigator !== 'undefined' && /iPhone|iPad|iPod/i.test(navigator.userAgent)) {
|
||||
return 'Safari 공유 메뉴에서 홈 화면에 추가를 선택하면 E-Reader 전용 아이콘으로 저장됩니다.';
|
||||
@@ -1002,7 +1016,10 @@ function getInstallGuideMessage() {
|
||||
return '브라우저 메뉴의 홈 화면에 추가 또는 앱 설치를 사용하면 E-Reader를 바로 열 수 있습니다.';
|
||||
}
|
||||
|
||||
async function createEReaderManifestObjectUrl(registeredToken: string) {
|
||||
async function createEReaderManifestObjectUrl(options?: {
|
||||
registeredToken?: string | null;
|
||||
shareToken?: string | null;
|
||||
}) {
|
||||
if (typeof window === 'undefined') {
|
||||
return '';
|
||||
}
|
||||
@@ -1020,8 +1037,17 @@ async function createEReaderManifestObjectUrl(registeredToken: string) {
|
||||
typeof manifest.start_url === 'string' && manifest.start_url.trim() ? manifest.start_url : getReaderLaunchPath(),
|
||||
window.location.origin,
|
||||
);
|
||||
const registeredToken = options?.registeredToken?.trim() ?? '';
|
||||
const shareToken = normalizeShareToken(options?.shareToken);
|
||||
|
||||
if (registeredToken) {
|
||||
startUrl.searchParams.set('registeredAccessToken', registeredToken);
|
||||
}
|
||||
|
||||
if (shareToken) {
|
||||
startUrl.searchParams.set('shareToken', shareToken);
|
||||
}
|
||||
|
||||
startUrl.searchParams.set('registeredAccessToken', registeredToken);
|
||||
manifest.start_url = `${startUrl.pathname}${startUrl.search}${startUrl.hash}`;
|
||||
|
||||
return window.URL.createObjectURL(
|
||||
@@ -1297,7 +1323,7 @@ function sleep(ms: number) {
|
||||
});
|
||||
}
|
||||
|
||||
export function EReaderAppView({ onBack, launchContext = 'direct' }: EReaderAppViewProps) {
|
||||
export function EReaderAppView({ onBack, launchContext = 'direct', shareToken }: EReaderAppViewProps) {
|
||||
const storedSettings = readStoredSettings();
|
||||
const initialStoredArticles = readStoredArticles();
|
||||
const initialReadHistory = readStoredReadHistory();
|
||||
@@ -1442,6 +1468,7 @@ export function EReaderAppView({ onBack, launchContext = 'direct' }: EReaderAppV
|
||||
const safeDisplayPageIndex = clampIndex(displayPageIndex, pageCount);
|
||||
const hiddenPageSlot = visiblePageSlot === 'primary' ? 'secondary' : 'primary';
|
||||
const isEmbeddedLaunch = launchContext === 'embedded';
|
||||
const effectiveShareToken = normalizeShareToken(shareToken) || readShareTokenFromUrl();
|
||||
const hasViewHistory = viewHistory.length > 0;
|
||||
const canExitToParentApp = isEmbeddedLaunch && installState !== 'standalone';
|
||||
const isStandaloneHome = installState === 'standalone' && currentView === 'home';
|
||||
@@ -1610,6 +1637,14 @@ export function EReaderAppView({ onBack, launchContext = 'direct' }: EReaderAppV
|
||||
};
|
||||
}, [librarySyncRequestId]);
|
||||
|
||||
useEffect(() => {
|
||||
setEReaderShareTokenOverride(effectiveShareToken);
|
||||
|
||||
return () => {
|
||||
setEReaderShareTokenOverride('');
|
||||
};
|
||||
}, [effectiveShareToken]);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof document === 'undefined') {
|
||||
return undefined;
|
||||
@@ -1621,10 +1656,13 @@ export function EReaderAppView({ onBack, launchContext = 'direct' }: EReaderAppV
|
||||
const previewRuntimeToken = isPreviewRuntime() ? getRegisteredAccessToken() : '';
|
||||
const restoreManifest = swapManifestForEReader();
|
||||
|
||||
if (previewRuntimeToken) {
|
||||
if (previewRuntimeToken || effectiveShareToken) {
|
||||
void (async () => {
|
||||
try {
|
||||
const manifestObjectUrl = await createEReaderManifestObjectUrl(previewRuntimeToken);
|
||||
const manifestObjectUrl = await createEReaderManifestObjectUrl({
|
||||
registeredToken: previewRuntimeToken,
|
||||
shareToken: effectiveShareToken,
|
||||
});
|
||||
|
||||
if (isDisposed) {
|
||||
window.URL.revokeObjectURL(manifestObjectUrl);
|
||||
@@ -1648,7 +1686,7 @@ export function EReaderAppView({ onBack, launchContext = 'direct' }: EReaderAppV
|
||||
restoreManifest();
|
||||
document.body.classList.remove(E_READER_IMMERSIVE_BODY_CLASS);
|
||||
};
|
||||
}, []);
|
||||
}, [effectiveShareToken]);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') {
|
||||
|
||||
@@ -92,6 +92,7 @@ const WORK_SERVER_FALLBACK_BASE_URL =
|
||||
!import.meta.env.VITE_WORK_SERVER_URL && WORK_SERVER_BASE_URL === '/api'
|
||||
? resolveWorkServerFallbackBaseUrl()
|
||||
: null;
|
||||
let eReaderShareTokenOverride = '';
|
||||
|
||||
class EReaderApiError extends Error {
|
||||
status: number;
|
||||
@@ -103,6 +104,26 @@ class EReaderApiError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeShareToken(value: string | null | undefined) {
|
||||
return value?.trim() ?? '';
|
||||
}
|
||||
|
||||
function readShareTokenFromUrl() {
|
||||
if (typeof window === 'undefined') {
|
||||
return '';
|
||||
}
|
||||
|
||||
return normalizeShareToken(new URLSearchParams(window.location.search).get('shareToken'));
|
||||
}
|
||||
|
||||
function resolveReaderShareToken() {
|
||||
return normalizeShareToken(eReaderShareTokenOverride) || readShareTokenFromUrl();
|
||||
}
|
||||
|
||||
export function setEReaderShareTokenOverride(shareToken: string | null | undefined) {
|
||||
eReaderShareTokenOverride = normalizeShareToken(shareToken);
|
||||
}
|
||||
|
||||
function parseJsonPayload<T>(text: string, fallbackMessage: string) {
|
||||
if (!text.trim()) {
|
||||
throw new EReaderApiError(fallbackMessage, 502);
|
||||
@@ -206,11 +227,16 @@ export async function extractReaderArticle(url: string) {
|
||||
function buildReaderHeaders() {
|
||||
const headers = appendClientIdHeader();
|
||||
const token = getRegisteredAccessToken();
|
||||
const shareToken = resolveReaderShareToken();
|
||||
|
||||
if (token && !headers.has('X-Access-Token')) {
|
||||
headers.set('X-Access-Token', token);
|
||||
}
|
||||
|
||||
if (shareToken && !headers.has('X-Chat-Share-Token')) {
|
||||
headers.set('X-Chat-Share-Token', shareToken);
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user