ddys-nextjs 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/.env.example +7 -0
  2. package/LICENSE +21 -0
  3. package/README.md +196 -0
  4. package/README.zh-CN.md +196 -0
  5. package/examples/app-router/app/api/ddys/[route]/route.ts +1 -0
  6. package/examples/app-router/app/api/ddys/diagnostics/route.ts +1 -0
  7. package/examples/app-router/app/api/ddys/request/route.ts +1 -0
  8. package/examples/app-router/app/api/ddys/revalidate/route.ts +1 -0
  9. package/examples/app-router/app/ddys/calendar/page.tsx +5 -0
  10. package/examples/app-router/app/ddys/collections/page.tsx +5 -0
  11. package/examples/app-router/app/ddys/diagnostics/page.tsx +5 -0
  12. package/examples/app-router/app/ddys/genres/page.tsx +5 -0
  13. package/examples/app-router/app/ddys/hot/page.tsx +5 -0
  14. package/examples/app-router/app/ddys/latest/page.tsx +5 -0
  15. package/examples/app-router/app/ddys/layout.tsx +13 -0
  16. package/examples/app-router/app/ddys/movie/[slug]/page.tsx +15 -0
  17. package/examples/app-router/app/ddys/movie/[slug]/sources/page.tsx +12 -0
  18. package/examples/app-router/app/ddys/movies/page.tsx +5 -0
  19. package/examples/app-router/app/ddys/regions/page.tsx +5 -0
  20. package/examples/app-router/app/ddys/request/page.tsx +8 -0
  21. package/examples/app-router/app/ddys/search/page.tsx +5 -0
  22. package/examples/app-router/app/ddys/shares/page.tsx +5 -0
  23. package/examples/app-router/app/ddys/types/page.tsx +5 -0
  24. package/examples/app-router/app/manifest.ts +9 -0
  25. package/examples/app-router/app/robots.ts +5 -0
  26. package/examples/app-router/app/sitemap.ts +7 -0
  27. package/next.config.example.mjs +9 -0
  28. package/package.json +105 -0
  29. package/public/images/icon-16.png +0 -0
  30. package/public/images/icon-192.png +0 -0
  31. package/public/images/icon-32.png +0 -0
  32. package/public/images/icon-512.png +0 -0
  33. package/public/images/logo.png +0 -0
  34. package/src/actions/index.ts +2 -0
  35. package/src/actions/request.ts +26 -0
  36. package/src/actions/revalidate.ts +15 -0
  37. package/src/client/client.ts +194 -0
  38. package/src/client/config.ts +111 -0
  39. package/src/client/error.ts +15 -0
  40. package/src/client/index.ts +13 -0
  41. package/src/components/card.tsx +38 -0
  42. package/src/components/client.ts +3 -0
  43. package/src/components/diagnostics.tsx +35 -0
  44. package/src/components/grid.tsx +27 -0
  45. package/src/components/index.ts +8 -0
  46. package/src/components/movie-detail.tsx +40 -0
  47. package/src/components/request-form.tsx +50 -0
  48. package/src/components/search.tsx +41 -0
  49. package/src/components/sources.tsx +27 -0
  50. package/src/components/utils.ts +50 -0
  51. package/src/components/view.tsx +50 -0
  52. package/src/index.ts +2 -0
  53. package/src/metadata/index.ts +273 -0
  54. package/src/route-handlers/diagnostics.ts +61 -0
  55. package/src/route-handlers/index.ts +10 -0
  56. package/src/route-handlers/proxy.ts +42 -0
  57. package/src/route-handlers/request.ts +46 -0
  58. package/src/route-handlers/revalidate.ts +31 -0
  59. package/src/server/cache.ts +45 -0
  60. package/src/server/client.ts +15 -0
  61. package/src/server/config.ts +49 -0
  62. package/src/server/index.ts +11 -0
  63. package/src/server/request-service.ts +105 -0
  64. package/src/styles/ddys.css +228 -0
  65. package/src/types/ddys.ts +99 -0
  66. package/src/utils/security.ts +125 -0
@@ -0,0 +1,31 @@
1
+ import type { DdysConfigInput } from '../client/config';
2
+ import { getDdysConfigFromEnv } from '../server/config';
3
+ import { revalidateDdys } from '../server/cache';
4
+
5
+ export interface DdysRevalidateRouteOptions {
6
+ config?: DdysConfigInput;
7
+ token?: string;
8
+ }
9
+
10
+ export function createDdysRevalidateRouteHandler(options: DdysRevalidateRouteOptions = {}) {
11
+ return async function POST(request: Request) {
12
+ const expected = options.token ?? process.env.DDYS_REVALIDATE_TOKEN ?? '';
13
+ const given = request.headers.get('x-ddys-revalidate-token') ?? new URL(request.url).searchParams.get('token') ?? '';
14
+ if (!expected || given !== expected) {
15
+ return Response.json({ success: false, message: 'Invalid revalidation token.' }, { status: 403 });
16
+ }
17
+ getDdysConfigFromEnv(options.config);
18
+ const body = await request.json().catch(() => ({}));
19
+ const tag = typeof body.tag === 'string' ? body.tag : undefined;
20
+ const tagProfile = typeof body.tagProfile === 'string' ? body.tagProfile : undefined;
21
+ const path = typeof body.path === 'string' ? body.path : undefined;
22
+ const pathType = body.pathType === 'layout' ? 'layout' : 'page';
23
+ if (!tag && !path) {
24
+ return Response.json({ success: false, message: 'Missing tag or path.' }, { status: 422 });
25
+ }
26
+ revalidateDdys({ tag, tagProfile, path, pathType });
27
+ return Response.json({ success: true, data: { tag, tagProfile, path, pathType } });
28
+ };
29
+ }
30
+
31
+ export const ddysRevalidatePOST = createDdysRevalidateRouteHandler();
@@ -0,0 +1,45 @@
1
+ import 'server-only';
2
+
3
+ import { revalidatePath, revalidateTag } from 'next/cache';
4
+ import type { DdysConfig } from '../client/config';
5
+
6
+ export interface DdysRevalidateInput {
7
+ tag?: string;
8
+ tagProfile?: string | { expire?: number };
9
+ path?: string;
10
+ pathType?: 'layout' | 'page';
11
+ }
12
+
13
+ export function ttlForPath(path: string, config: DdysConfig): number {
14
+ if (/^\/(types|genres|regions|calendar)$/.test(path)) return config.cache.dictionaryTtl;
15
+ if (/^\/(latest|hot)$/.test(path)) return config.cache.freshTtl;
16
+ if (/^\/(movies\/[^/]+|movies\/[^/]+\/sources|movies\/[^/]+\/related|collections\/[^/]+|shares\/[0-9]+)$/.test(path)) return config.cache.detailTtl;
17
+ if (/^\/(movies\/[^/]+\/comments|suggest|shares|requests|activities|user\/)/.test(path)) return config.cache.communityTtl;
18
+ if (/^\/(movies|search|collections)/.test(path)) return config.cache.listTtl;
19
+ return config.cache.defaultTtl;
20
+ }
21
+
22
+ export function tagsForPath(path: string): string[] {
23
+ const tags = ['ddys'];
24
+ if (/^\/latest/.test(path)) tags.push('ddys:latest');
25
+ if (/^\/hot/.test(path)) tags.push('ddys:hot');
26
+ if (/^\/movies$/.test(path)) tags.push('ddys:movies');
27
+ if (/^\/movies\/([^/]+)/.test(path)) tags.push(`ddys:movie:${path.split('/')[2]}`);
28
+ if (/^\/(types|genres|regions|calendar)$/.test(path)) tags.push('ddys:dictionary');
29
+ if (/^\/(shares|requests|activities|user\/|movies\/[^/]+\/comments)/.test(path)) tags.push('ddys:community');
30
+ return tags;
31
+ }
32
+
33
+ export function nextFetchOptions(path: string, config: DdysConfig) {
34
+ return {
35
+ next: {
36
+ revalidate: ttlForPath(path, config),
37
+ tags: tagsForPath(path)
38
+ }
39
+ };
40
+ }
41
+
42
+ export function revalidateDdys(input: DdysRevalidateInput) {
43
+ if (input.tag) revalidateTag(input.tag, input.tagProfile ?? 'max');
44
+ if (input.path) revalidatePath(input.path, input.pathType ?? 'page');
45
+ }
@@ -0,0 +1,15 @@
1
+ import 'server-only';
2
+
3
+ import { DdysClient } from '../client/client';
4
+ import type { DdysConfigInput } from '../client/config';
5
+ import { getDdysConfigFromEnv } from './config';
6
+ import { nextFetchOptions } from './cache';
7
+
8
+ export function createDdysServerClient(config: DdysConfigInput = {}) {
9
+ return new DdysClient(getDdysConfigFromEnv(config));
10
+ }
11
+
12
+ export function withDdysCache(path: string, config: DdysConfigInput = {}) {
13
+ const merged = getDdysConfigFromEnv(config);
14
+ return nextFetchOptions(path, merged);
15
+ }
@@ -0,0 +1,49 @@
1
+ import 'server-only';
2
+
3
+ import { DEFAULT_DDYS_CONFIG, mergeDdysConfig, type DdysConfig, type DdysConfigInput } from '../client/config';
4
+ import { boolValue, intRange, normalizeBaseUrl } from '../utils/security';
5
+
6
+ export function getDdysConfigFromEnv(input: DdysConfigInput = {}): DdysConfig {
7
+ const env = process.env;
8
+ const envConfig: DdysConfigInput = {
9
+ apiBaseUrl: normalizeBaseUrl(env.DDYS_API_BASE_URL, DEFAULT_DDYS_CONFIG.apiBaseUrl),
10
+ siteBaseUrl: normalizeBaseUrl(env.DDYS_SITE_BASE_URL, DEFAULT_DDYS_CONFIG.siteBaseUrl),
11
+ apiKey: env.DDYS_API_KEY ?? '',
12
+ timeout: intRange(env.DDYS_TIMEOUT, DEFAULT_DDYS_CONFIG.timeout, 1, 60),
13
+ retryTimes: intRange(env.DDYS_RETRY_TIMES, DEFAULT_DDYS_CONFIG.retryTimes, 0, 5),
14
+ retrySleep: intRange(env.DDYS_RETRY_SLEEP, DEFAULT_DDYS_CONFIG.retrySleep, 0, 3000),
15
+ requestForm: {
16
+ enabled: boolValue(env.DDYS_REQUEST_FORM_ENABLED),
17
+ secret: env.DDYS_FORM_SECRET,
18
+ csrf: env.DDYS_REQUEST_FORM_CSRF === undefined ? true : boolValue(env.DDYS_REQUEST_FORM_CSRF)
19
+ },
20
+ diagnostics: {
21
+ enabled: boolValue(env.DDYS_DIAGNOSTICS_ENABLED)
22
+ }
23
+ };
24
+
25
+ return mergeDdysConfig({
26
+ ...envConfig,
27
+ ...input,
28
+ cache: { ...envConfig.cache, ...input.cache },
29
+ proxy: { ...envConfig.proxy, ...input.proxy },
30
+ requestForm: { ...envConfig.requestForm, ...input.requestForm },
31
+ diagnostics: { ...envConfig.diagnostics, ...input.diagnostics },
32
+ security: { ...envConfig.security, ...input.security }
33
+ });
34
+ }
35
+
36
+ export function requireDdysApiKey(config: DdysConfig): void {
37
+ if (!config.apiKey) throw new Error('DDYS_API_KEY is required for this server action.');
38
+ }
39
+
40
+ export function safeDdysConfig(config: DdysConfig) {
41
+ return {
42
+ ...config,
43
+ apiKey: config.apiKey ? 'configured' : 'not configured',
44
+ requestForm: {
45
+ ...config.requestForm,
46
+ secret: config.requestForm.secret ? 'configured' : 'not configured'
47
+ }
48
+ };
49
+ }
@@ -0,0 +1,11 @@
1
+ export { createDdysServerClient, withDdysCache } from './client';
2
+ export { getDdysConfigFromEnv, requireDdysApiKey, safeDdysConfig } from './config';
3
+ export { nextFetchOptions, revalidateDdys, tagsForPath, ttlForPath } from './cache';
4
+ export {
5
+ createRequestFormToken,
6
+ enforceRateLimit,
7
+ normalizeRequestInput,
8
+ submitDdysRequest,
9
+ verifyRequestFormToken,
10
+ type DdysRequestSubmitOptions
11
+ } from './request-service';
@@ -0,0 +1,105 @@
1
+ import 'server-only';
2
+
3
+ import type { DdysConfig } from '../client/config';
4
+ import { DdysClient } from '../client/client';
5
+ import type { DdysRequestInput } from '../types/ddys';
6
+ import { scalar } from '../utils/security';
7
+
8
+ const globalStore = globalThis as typeof globalThis & {
9
+ __ddysNextRateLimit?: Map<string, number>;
10
+ };
11
+
12
+ export interface DdysRequestSubmitOptions {
13
+ identity: string;
14
+ token?: string;
15
+ }
16
+
17
+ export async function submitDdysRequest(input: Record<string, unknown>, config: DdysConfig, options: DdysRequestSubmitOptions) {
18
+ if (!config.requestForm.enabled) throw new Error('DDYS request form is disabled.');
19
+ if (config.requestForm.csrf && !(await verifyRequestFormToken(options.token || '', config, options.identity))) {
20
+ throw new Error('Invalid request token.');
21
+ }
22
+ const honeypot = scalar(input[config.requestForm.honeypotField]);
23
+ if (honeypot !== '') throw new Error('Invalid submission.');
24
+ enforceRateLimit(options.identity, config.requestForm.rateLimitSeconds);
25
+ const payload = normalizeRequestInput(input);
26
+ return new DdysClient(config).createRequest(payload);
27
+ }
28
+
29
+ export function normalizeRequestInput(input: Record<string, unknown>): DdysRequestInput {
30
+ const title = scalar(input.title).slice(0, 255);
31
+ if (!title) throw new Error('Title is required.');
32
+ const year = scalar(input.year);
33
+ if (year && (!/^\d{4}$/.test(year) || Number(year) < 1900 || Number(year) > 2099)) throw new Error('Invalid year.');
34
+ const type = scalar(input.type).toLowerCase();
35
+ if (type && !['movie', 'series', 'variety', 'anime'].includes(type)) throw new Error('Invalid type.');
36
+ const douban = scalar(input.douban_id);
37
+ if (douban && !/^\d{1,20}$/.test(douban)) throw new Error('Invalid Douban ID.');
38
+ const imdb = scalar(input.imdb_id);
39
+ if (imdb && !/^tt\d{1,20}$/i.test(imdb)) throw new Error('Invalid IMDb ID.');
40
+
41
+ return Object.fromEntries(Object.entries({
42
+ title,
43
+ year: year ? Number(year) : undefined,
44
+ type: type || undefined,
45
+ description: scalar(input.description).slice(0, 1000) || undefined,
46
+ douban_id: douban || undefined,
47
+ imdb_id: imdb || undefined,
48
+ site: 'Next.js'
49
+ }).filter(([, value]) => value !== undefined && value !== '')) as DdysRequestInput;
50
+ }
51
+
52
+ export function enforceRateLimit(identity: string, seconds: number): void {
53
+ globalStore.__ddysNextRateLimit ??= new Map();
54
+ const key = `ddys:${identity}`;
55
+ const now = Date.now();
56
+ const last = globalStore.__ddysNextRateLimit.get(key) ?? 0;
57
+ if (now - last < seconds * 1000) throw new Error('Too many submissions. Please try again later.');
58
+ globalStore.__ddysNextRateLimit.set(key, now);
59
+ }
60
+
61
+ export async function createRequestFormToken(config: DdysConfig, identity = 'anonymous', now = Date.now()): Promise<string> {
62
+ const secret = formSecret(config);
63
+ if (!secret) return '';
64
+ const bucket = Math.floor(now / (config.requestForm.tokenTtlSeconds * 1000));
65
+ const signature = await hmac(secret, `ddys-request:${identity}:${bucket}`);
66
+ return `${bucket}.${signature}`;
67
+ }
68
+
69
+ export async function verifyRequestFormToken(token: string, config: DdysConfig, identity = 'anonymous', now = Date.now()): Promise<boolean> {
70
+ const secret = formSecret(config);
71
+ if (!secret) return false;
72
+ const [bucketText, signature] = token.split('.', 2);
73
+ const bucket = Number(bucketText);
74
+ if (!Number.isInteger(bucket) || !signature) return false;
75
+ const current = Math.floor(now / (config.requestForm.tokenTtlSeconds * 1000));
76
+ if (bucket < current - 1 || bucket > current) return false;
77
+ const expected = await hmac(secret, `ddys-request:${identity}:${bucket}`);
78
+ return timingSafeEqual(signature, expected);
79
+ }
80
+
81
+ function formSecret(config: DdysConfig): string {
82
+ return config.requestForm.secret || config.apiKey || '';
83
+ }
84
+
85
+ async function hmac(secret: string, value: string): Promise<string> {
86
+ if (!globalThis.crypto?.subtle) {
87
+ throw new Error('Web Crypto is required to sign DDYS request form tokens.');
88
+ }
89
+ const key = await globalThis.crypto.subtle.importKey(
90
+ 'raw',
91
+ new TextEncoder().encode(secret),
92
+ { name: 'HMAC', hash: 'SHA-256' },
93
+ false,
94
+ ['sign']
95
+ );
96
+ const signature = await globalThis.crypto.subtle.sign('HMAC', key, new TextEncoder().encode(value));
97
+ return Array.from(new Uint8Array(signature)).map((byte) => byte.toString(16).padStart(2, '0')).join('');
98
+ }
99
+
100
+ function timingSafeEqual(a: string, b: string): boolean {
101
+ if (a.length !== b.length) return false;
102
+ let out = 0;
103
+ for (let i = 0; i < a.length; i++) out |= a.charCodeAt(i) ^ b.charCodeAt(i);
104
+ return out === 0;
105
+ }
@@ -0,0 +1,228 @@
1
+ .ddys-next {
2
+ color: #17202a;
3
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
4
+ line-height: 1.55;
5
+ }
6
+
7
+ .ddys-next-items {
8
+ display: grid;
9
+ gap: 16px;
10
+ grid-template-columns: repeat(var(--ddys-next-columns, 4), minmax(0, 1fr));
11
+ }
12
+
13
+ .ddys-next-layout-list .ddys-next-items,
14
+ .ddys-next-layout-compact .ddys-next-items {
15
+ grid-template-columns: 1fr;
16
+ }
17
+
18
+ .ddys-next-card {
19
+ background: #fff;
20
+ border: 1px solid #d6e0e8;
21
+ border-radius: 8px;
22
+ display: grid;
23
+ grid-template-rows: auto 1fr;
24
+ min-width: 0;
25
+ overflow: hidden;
26
+ }
27
+
28
+ .ddys-next-layout-list .ddys-next-card {
29
+ grid-template-columns: minmax(120px, 180px) minmax(0, 1fr);
30
+ grid-template-rows: 1fr;
31
+ }
32
+
33
+ .ddys-next-poster {
34
+ aspect-ratio: 2 / 3;
35
+ background: #eef3f6;
36
+ overflow: hidden;
37
+ }
38
+
39
+ .ddys-next-poster img {
40
+ display: block;
41
+ height: 100%;
42
+ object-fit: cover;
43
+ width: 100%;
44
+ }
45
+
46
+ .ddys-next-card-body {
47
+ display: grid;
48
+ gap: 8px;
49
+ min-width: 0;
50
+ padding: 12px;
51
+ }
52
+
53
+ .ddys-next-title {
54
+ font-size: 16px;
55
+ line-height: 1.3;
56
+ margin: 0;
57
+ overflow-wrap: anywhere;
58
+ }
59
+
60
+ .ddys-next-title a {
61
+ color: #0d5e8c;
62
+ text-decoration: none;
63
+ }
64
+
65
+ .ddys-next-title a:hover {
66
+ text-decoration: underline;
67
+ }
68
+
69
+ .ddys-next-meta,
70
+ .ddys-next-summary,
71
+ .ddys-next-status {
72
+ color: #586777;
73
+ font-size: 13px;
74
+ }
75
+
76
+ .ddys-next-empty,
77
+ .ddys-next-description,
78
+ .ddys-next-source-group,
79
+ .ddys-next-diagnostics {
80
+ background: #fff;
81
+ border: 1px solid #d6e0e8;
82
+ border-radius: 8px;
83
+ padding: 16px;
84
+ }
85
+
86
+ .ddys-next-detail,
87
+ .ddys-next-sources,
88
+ .ddys-next-related {
89
+ display: grid;
90
+ gap: 16px;
91
+ }
92
+
93
+ .ddys-next-resource {
94
+ display: flex;
95
+ flex-wrap: wrap;
96
+ gap: 8px;
97
+ }
98
+
99
+ .ddys-next-resource a,
100
+ .ddys-next button {
101
+ border: 1px solid #c9d7e3;
102
+ border-radius: 8px;
103
+ color: #17202a;
104
+ display: inline-flex;
105
+ font-size: 14px;
106
+ font-weight: 650;
107
+ min-height: 38px;
108
+ padding: 8px 12px;
109
+ text-decoration: none;
110
+ }
111
+
112
+ .ddys-next button {
113
+ background: #17324d;
114
+ border-color: #17324d;
115
+ color: #fff;
116
+ cursor: pointer;
117
+ }
118
+
119
+ .ddys-next-search,
120
+ .ddys-next-request-form {
121
+ align-items: end;
122
+ display: grid;
123
+ gap: 12px;
124
+ grid-template-columns: repeat(3, minmax(0, 1fr));
125
+ }
126
+
127
+ .ddys-next-request-form {
128
+ grid-template-columns: repeat(2, minmax(0, 1fr));
129
+ }
130
+
131
+ .ddys-next-search input,
132
+ .ddys-next-search select,
133
+ .ddys-next-request-form input,
134
+ .ddys-next-request-form select,
135
+ .ddys-next-request-form textarea {
136
+ border: 1px solid #b9cad8;
137
+ border-radius: 8px;
138
+ box-sizing: border-box;
139
+ font: inherit;
140
+ min-height: 40px;
141
+ padding: 8px 10px;
142
+ width: 100%;
143
+ }
144
+
145
+ .ddys-next-request-form label {
146
+ display: grid;
147
+ gap: 6px;
148
+ min-width: 0;
149
+ }
150
+
151
+ .ddys-next-request-form textarea {
152
+ min-height: 104px;
153
+ resize: vertical;
154
+ }
155
+
156
+ .ddys-next-honeypot {
157
+ height: 1px;
158
+ left: -10000px;
159
+ overflow: hidden;
160
+ position: absolute;
161
+ top: auto;
162
+ width: 1px;
163
+ }
164
+
165
+ .ddys-next-status {
166
+ margin: 0;
167
+ min-height: 22px;
168
+ }
169
+
170
+ .ddys-next-status.is-error {
171
+ color: #8f2f16;
172
+ }
173
+
174
+ .ddys-next-diagnostics {
175
+ display: grid;
176
+ gap: 12px;
177
+ }
178
+
179
+ .ddys-next-actions {
180
+ display: flex;
181
+ flex-wrap: wrap;
182
+ gap: 8px;
183
+ }
184
+
185
+ .ddys-next-diagnostics pre {
186
+ overflow: auto;
187
+ white-space: pre-wrap;
188
+ }
189
+
190
+ @media (max-width: 900px) {
191
+ .ddys-next-items {
192
+ grid-template-columns: repeat(2, minmax(0, 1fr));
193
+ }
194
+ }
195
+
196
+ @media (max-width: 640px) {
197
+ .ddys-next-items,
198
+ .ddys-next-layout-list .ddys-next-card,
199
+ .ddys-next-search,
200
+ .ddys-next-request-form {
201
+ grid-template-columns: 1fr;
202
+ }
203
+ }
204
+
205
+ @media (prefers-color-scheme: dark) {
206
+ .ddys-next-theme-auto {
207
+ color: #ecf2f6;
208
+ }
209
+
210
+ .ddys-next-theme-auto .ddys-next-card,
211
+ .ddys-next-theme-auto .ddys-next-empty,
212
+ .ddys-next-theme-auto .ddys-next-description,
213
+ .ddys-next-theme-auto .ddys-next-source-group,
214
+ .ddys-next-theme-auto .ddys-next-diagnostics {
215
+ background: #151f29;
216
+ border-color: #304253;
217
+ }
218
+
219
+ .ddys-next-theme-auto .ddys-next-meta,
220
+ .ddys-next-theme-auto .ddys-next-summary,
221
+ .ddys-next-theme-auto .ddys-next-status {
222
+ color: #a7b7c3;
223
+ }
224
+
225
+ .ddys-next-theme-auto .ddys-next-title a {
226
+ color: #83c7e8;
227
+ }
228
+ }
@@ -0,0 +1,99 @@
1
+ export type DdysScalar = string | number | boolean | null | undefined;
2
+
3
+ export interface DdysQuery {
4
+ [key: string]: DdysScalar | DdysScalar[];
5
+ }
6
+
7
+ export interface DdysApiResponse<T = unknown> {
8
+ success?: boolean;
9
+ message?: string;
10
+ data?: T;
11
+ meta?: DdysPaginationMeta;
12
+ [key: string]: unknown;
13
+ }
14
+
15
+ export interface DdysPaginationMeta {
16
+ page?: number;
17
+ per_page?: number;
18
+ total?: number;
19
+ total_pages?: number;
20
+ [key: string]: unknown;
21
+ }
22
+
23
+ export interface DdysPaginated<T = DdysItem> {
24
+ data: T[];
25
+ meta: DdysPaginationMeta;
26
+ }
27
+
28
+ export interface DdysItem {
29
+ id?: string | number;
30
+ slug?: string;
31
+ title?: string;
32
+ name?: string;
33
+ cn_name?: string;
34
+ en_name?: string;
35
+ username?: string;
36
+ search_keyword?: string;
37
+ poster?: string;
38
+ cover?: string;
39
+ image?: string;
40
+ avatar?: string;
41
+ url?: string;
42
+ link?: string;
43
+ href?: string;
44
+ year?: string | number;
45
+ type?: string;
46
+ type_code?: string;
47
+ genre?: string | string[];
48
+ region?: string | string[];
49
+ quality?: string;
50
+ episode?: string | number;
51
+ status?: string;
52
+ rating?: string | number;
53
+ description?: string;
54
+ intro?: string;
55
+ summary?: string;
56
+ note?: string;
57
+ content?: string;
58
+ bio?: string;
59
+ [key: string]: unknown;
60
+ }
61
+
62
+ export interface DdysResource {
63
+ title?: string;
64
+ name?: string;
65
+ label?: string;
66
+ download_type?: string;
67
+ type?: string;
68
+ quality?: string;
69
+ url?: string;
70
+ link?: string;
71
+ href?: string;
72
+ [key: string]: unknown;
73
+ }
74
+
75
+ export interface DdysRequestInput {
76
+ title: string;
77
+ year?: string | number;
78
+ type?: 'movie' | 'series' | 'variety' | 'anime' | '';
79
+ description?: string;
80
+ douban_id?: string;
81
+ imdb_id?: string;
82
+ site?: string;
83
+ [key: string]: unknown;
84
+ }
85
+
86
+ export interface DdysDisplayOptions {
87
+ layout?: 'grid' | 'list' | 'compact';
88
+ theme?: 'auto' | 'light' | 'dark';
89
+ columns?: number;
90
+ target?: '_blank' | '_self';
91
+ showPoster?: boolean;
92
+ showRating?: boolean;
93
+ showSummary?: boolean;
94
+ showSourceLink?: boolean;
95
+ }
96
+
97
+ export interface DdysRouteHandlerContext {
98
+ params?: Record<string, string> | Promise<Record<string, string>>;
99
+ }