diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..0465677e --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,28 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node +{ + "name": "Node.js & TypeScript", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "image": "mcr.microsoft.com/devcontainers/typescript-node:20", + "features": { + "ghcr.io/devcontainers/features/node:1": { + "version": "latest", + "nvmVersion": "latest" + } + } + + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "yarn install", + + // Configure tool-specific properties. + // "customizations": {}, + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} diff --git a/.env.sample b/.env.sample index a51af8a1..db8dd629 100644 --- a/.env.sample +++ b/.env.sample @@ -1,5 +1,3 @@ -VITE_ADSENSE_PUB_ID = -VITE_GOOGLE_ANALYTICS_ID = -VITE_GOOGLE_SEARCH_CONSOLE_VERIFICATION = -VITE_PXIMG_BASEURL_I = /-/ -VITE_PXIMG_BASEURL_S = /~/ +NUXT_ADSENSE_PUB_ID = +NUXT_GOOGLE_ANALYTICS_ID = +NUXT_GOOGLE_SEARCH_CONSOLE_VERIFICATION = diff --git a/.npmrc b/.npmrc index 9a0c9516..0ba08b34 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1,2 @@ +shamefully-hoist = true registry = https://registry.npmmirror.com diff --git a/.vscode/vue.code-snippets b/.vscode/vue.code-snippets deleted file mode 100644 index abd10498..00000000 --- a/.vscode/vue.code-snippets +++ /dev/null @@ -1,19 +0,0 @@ -{ - "Init vue components": { - "scope": "vue", - "prefix": "vue", - "body": [ - "", - "", - "", - "", - "" - ], - "description": "Init vue components" - } -} \ No newline at end of file diff --git a/README.md b/README.md index 1f0dcb8c..1ee1775e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -
+
-![PixivNow Logo](src/assets/LogoH.png) +![PixivNow Logo](public/images/LogoH.png) Pixiv Service Proxy diff --git a/api/http.ts b/api/http.ts deleted file mode 100644 index f74dd74f..00000000 --- a/api/http.ts +++ /dev/null @@ -1,43 +0,0 @@ -import type { VercelRequest, VercelResponse } from '@vercel/node' -import escapeRegExp from 'lodash.escaperegexp' -import { ajax } from './utils.js' - -export default async function (req: VercelRequest, res: VercelResponse) { - if (!isAccepted(req)) { - return res.status(403).send('403') - } - - try { - const { __PREFIX, __PATH } = req.query - const { data } = await ajax({ - method: req.method ?? 'GET', - url: `/${encodeURI(`${__PREFIX}${__PATH ? '/' + __PATH : ''}`)}`, - params: req.query ?? {}, - data: req.body || undefined, - headers: req.headers as Record, - }) - res.status(200).send(data) - } catch (e: any) { - res.status(e?.response?.status || 500).send(e?.response?.data || e) - } -} - -function isAccepted(req: VercelRequest) { - const { UA_BLACKLIST = '[]' } = process.env - try { - const list: string[] = JSON.parse(UA_BLACKLIST) - const ua = req.headers['user-agent'] ?? '' - return ( - !!ua && - Array.isArray(list) && - (list.length > 0 - ? !new RegExp( - `(${list.map((str) => escapeRegExp(str)).join('|')})`, - 'gi' - ).test(ua) - : true) - ) - } catch (e) { - return false - } -} diff --git a/api/image.ts b/api/image.ts deleted file mode 100644 index 8c87b104..00000000 --- a/api/image.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { VercelRequest, VercelResponse } from '@vercel/node' -import axios from 'axios' -import { USER_AGENT } from './utils.js' - -export default async (req: VercelRequest, res: VercelResponse) => { - const { __PREFIX, __PATH } = req.query - if (!__PREFIX || !__PATH) { - return res.status(400).send({ message: 'Missing param(s)' }) - } - - switch (__PREFIX) { - case '~': { - return axios - .get(`https://s.pximg.net/${__PATH}`, { - responseType: 'arraybuffer', - headers: { - referer: 'https://www.pixiv.net/', - 'user-agent': USER_AGENT, - }, - }) - .then( - ({ data, headers }) => { - res.setHeader('Content-Type', headers['content-type']) - res.setHeader( - 'Cache-Control', - `public, max-age=${12 * 60 * 60 * 3600}` - ) - res.status(200).send(Buffer.from(data)) - }, - (err) => { - return res - .status(err?.response?.status || 500) - .send(err?.response?.data || err) - } - ) - } - default: - return res.status(400).send({ message: 'Invalid request' }) - } -} diff --git a/api/random.ts b/api/random.ts deleted file mode 100644 index 51bdeed7..00000000 --- a/api/random.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { VercelRequest, VercelResponse } from '@vercel/node' -import { formatInTimeZone } from 'date-fns-tz' -import { PXIMG_BASEURL_I, ajax } from './utils.js' -import { Artwork } from '../src/types/Artworks.js' - -type ArtworkOrAd = Artwork | { isAdContainer: boolean } - -export default async (req: VercelRequest, res: VercelResponse) => { - const requestImage = - (req.headers.accept?.includes('image') || req.query.format === 'image') && - req.query.format !== 'json' - try { - const data: { illusts?: ArtworkOrAd[] } = ( - await ajax({ - url: '/ajax/illust/discovery', - params: { - mode: req.query.mode ?? 'safe', - max: requestImage ? '1' : req.query.max ?? '18', - }, - headers: req.headers, - }) - ).data - const illusts = (data.illusts ?? []).filter((value): value is Artwork => - Object.keys(value).includes('id') - ) - illusts.forEach((value) => { - const middle = `img/${formatInTimeZone( - value.updateDate, - 'Asia/Tokyo', - 'yyyy/MM/dd/HH/mm/ss' - )}/${value.id}` - value.urls = { - mini: `${PXIMG_BASEURL_I}c/48x48/img-master/${middle}_p0_square1200.jpg`, - thumb: `${PXIMG_BASEURL_I}c/250x250_80_a2/img-master/${middle}_p0_square1200.jpg`, - small: `${PXIMG_BASEURL_I}c/540x540_70/img-master/${middle}_p0_master1200.jpg`, - regular: `${PXIMG_BASEURL_I}img-master/${middle}_p0_master1200.jpg`, - original: `${PXIMG_BASEURL_I}img-original/${middle}_p0.jpg`, - } - }) - if (requestImage) { - res.redirect(illusts[0].urls.regular) - return - } else { - res.send(illusts) - return - } - } catch (e: any) { - res.status(e?.response?.status ?? 500).send(e?.response?.data ?? e) - } -} diff --git a/api/user.ts b/api/user.ts deleted file mode 100644 index 3ab88501..00000000 --- a/api/user.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { VercelRequest, VercelResponse } from '@vercel/node' -import { load } from 'cheerio' -import { ajax, replacePximgUrlsInObject } from './utils.js' - -export default async (req: VercelRequest, res: VercelResponse) => { - const token = req.cookies.PHPSESSID || req.query.token - if (!token) { - return res.status(403).send({ message: '未配置用户密钥' }) - } - - ajax - .get('/', { params: req.query, headers: req.headers }) - .then(async ({ data }) => { - const $ = load(data) - const $meta = $('meta[name="global-data"]') - if ($meta.length < 0 || !$meta.attr('content')) { - return res.status(403).send({ message: '无效的用户密钥' }) - } - - let meta - try { - meta = JSON.parse($meta.attr('content') as string) - } catch (error) { - res.status(403).send({ - message: '意料外的元数据', - cheerio: { - length: $meta.length, - html: $meta.prop('outerHTML'), - }, - error, - }) - - return - } - - if (!meta.userData) { - res.status(403).send({ - message: '无法获取登录状态', - meta, - }) - return - } - - res.setHeader('cache-control', 'no-cache') - res.setHeader( - 'set-cookie', - `CSRFTOKEN=${meta.token}; path=/; secure; sameSite=Lax` - ) - res.send(replacePximgUrlsInObject(meta)) - }) - .catch((err) => { - return res - .status(err?.response?.status || 500) - .send(err?.response?.data || err) - }) -} diff --git a/api/utils.ts b/api/utils.ts deleted file mode 100644 index b26b3ea1..00000000 --- a/api/utils.ts +++ /dev/null @@ -1,186 +0,0 @@ -import { VercelRequest, VercelResponse } from '@vercel/node' -import axios from 'axios' -import colors from 'picocolors' - -// HTTP handler -export default async function (req: VercelRequest, res: VercelResponse) { - res.status(404).send({ - error: true, - message: 'Not Found', - body: null, - }) -} - -export const PROD = process.env.NODE_ENV === 'production' -export const DEV = !PROD -export const USER_AGENT = - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0' -export const PXIMG_BASEURL_I = (() => { - const i = process.env.VITE_PXIMG_BASEURL_I - return i ? i.replace(/\/$/, '') + '/' : 'https://i.pximg.net/' -})() -export const PXIMG_BASEURL_S = (() => { - const s = process.env.VITE_PXIMG_BASEURL_S - return s ? s.replace(/\/$/, '') + '/' : 'https://s.pximg.net/' -})() - -export class CookieUtils { - static toJSON(raw: string) { - return Object.fromEntries(new URLSearchParams(raw.replace(/;\s*/g, '&'))) - } - static toString(obj: any) { - return Object.keys(obj) - .map((i) => `${i}=${obj[i]}`) - .join(';') - } -} - -export const ajax = axios.create({ - baseURL: 'https://www.pixiv.net/', - params: {}, - headers: { - 'user-agent': USER_AGENT, - }, - timeout: 9 * 1000, -}) -ajax.interceptors.request.use((ctx) => { - // 去除内部参数 - ctx.params = ctx.params || {} - delete ctx.params.__PATH - delete ctx.params.__PREFIX - - const cookies = CookieUtils.toJSON(ctx.headers.cookie || '') - const csrfToken = ctx.headers['x-csrf-token'] ?? cookies.CSRFTOKEN ?? '' - // 强制覆写部分 headers - ctx.headers = ctx.headers || {} - ctx.headers.host = 'www.pixiv.net' - ctx.headers.origin = 'https://www.pixiv.net' - ctx.headers.referer = 'https://www.pixiv.net/' - ctx.headers['user-agent'] = USER_AGENT - ctx.headers['accept-language'] ??= - 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6' - csrfToken && (ctx.headers['x-csrf-token'] = csrfToken) - - if (DEV) { - console.info( - colors.green(`[${ctx.method?.toUpperCase()}] <`), - colors.cyan(ctx.url || '') - ) - console.info({ - params: ctx.params, - data: ctx.data, - cookies, - }) - } - - return ctx -}) -ajax.interceptors.response.use((ctx) => { - typeof ctx.data === 'object' && - (ctx.data = replacePximgUrlsInObject(ctx.data?.body ?? ctx.data)) - if (DEV) { - const out: string = - typeof ctx.data === 'object' - ? JSON.stringify(ctx.data, null, 2) - : ctx.data.toString().trim() - console.info( - colors.green('[SEND] >'), - colors.cyan(ctx.request?.path?.replace('https://www.pixiv.net', '')), - `\n${colors.yellow(typeof ctx.data)} ${ - out.length >= 200 ? out.slice(0, 200).trim() + '\n...' : out - }` - ) - } - return ctx -}) - -export function replacePximgUrlsInString(str: string): string { - if (!str.includes('pximg.net')) return str - return str - .replaceAll('https://i.pximg.net/', PXIMG_BASEURL_I) - .replaceAll('https://s.pximg.net/', PXIMG_BASEURL_S) -} - -export function replacePximgUrlsInObject( - obj: Record | string -): Record | string { - if (typeof obj === 'string') return replacePximgUrlsInString(obj) - - return deepReplaceString(obj, replacePximgUrlsInString) -} - -function isObject(value: any): value is Record { - return typeof value === 'object' && value !== null -} - -export function deepReplaceString( - obj: T, - replacer: (value: string) => string -): T { - if (Array.isArray(obj)) { - return obj.map((value) => - deepReplaceString(value, replacer) - ) as unknown as T - } else if (isObject(obj)) { - if ( - ['arraybuffer', 'blob', 'formdata'].includes( - obj.constructor.name.toLowerCase() - ) - ) { - return obj - } - const result: Record = {} - for (const [key, value] of Object.entries(obj)) { - result[key] = deepReplaceString(value, replacer) - } - return result as T - } else if (typeof obj === 'string') { - return replacer(obj) as unknown as T - } - return obj -} - -export function safelyStringify(value: any, space?: number) { - const visited = new WeakSet() - - const replacer = (key: string, val: any) => { - // 处理 BigInt - if (typeof val === 'bigint') { - return val.toString() - } - - // 处理 Set - if (val instanceof Set) { - return Array.from(val) - } - - // 处理 Map - if (val instanceof Map) { - return Array.from(val.entries()) - } - - // 处理 function - if (typeof val === 'function') { - return val.toString() - } - - // 处理自循环引用 - if (typeof val === 'object' && val !== null) { - if (visited.has(val)) { - return '' - } - visited.add(val) - } - - return val - } - - return JSON.stringify(value, replacer, space) -} - -JSON.safelyStringify = safelyStringify -declare global { - interface JSON { - safelyStringify: typeof safelyStringify - } -} diff --git a/app/app.config.ts b/app/app.config.ts new file mode 100644 index 00000000..2d8fbc69 --- /dev/null +++ b/app/app.config.ts @@ -0,0 +1,15 @@ +import { version } from '../package.json' + +export default defineAppConfig({ + version, + githubOwner: 'FreeNowOrg', + githubRepo: 'PixivNow', + githubUrl: 'https://github.com/FreeNowOrg/PixivNow', + projectName: 'PixivNow', + projectTagline: 'Enjoy Pixiv Now (pixiv.js.org)', + imageCacheSeconds: 12 * 60 * 60 * 1000, + siteEnv: + process.env.NODE_ENV === 'development' || version.includes('-') + ? 'development' + : 'production', +}) diff --git a/app/app.vue b/app/app.vue new file mode 100644 index 00000000..d5e48983 --- /dev/null +++ b/app/app.vue @@ -0,0 +1,65 @@ + + + + + diff --git a/src/styles/animate.sass b/app/assets/styles/animate.sass similarity index 100% rename from src/styles/animate.sass rename to app/assets/styles/animate.sass diff --git a/src/styles/elements.sass b/app/assets/styles/elements.sass similarity index 100% rename from src/styles/elements.sass rename to app/assets/styles/elements.sass diff --git a/src/styles/formats.sass b/app/assets/styles/formats.sass similarity index 100% rename from src/styles/formats.sass rename to app/assets/styles/formats.sass diff --git a/src/styles/index.sass b/app/assets/styles/index.sass similarity index 97% rename from src/styles/index.sass rename to app/assets/styles/index.sass index 0a9f735b..0aaefd81 100644 --- a/src/styles/index.sass +++ b/app/assets/styles/index.sass @@ -12,7 +12,7 @@ body * box-sizing: border-box -#app +#app-full-container font-family: Avenir, Helvetica, Arial, sans-serif -webkit-font-smoothing: antialiased -moz-osx-font-smoothing: grayscale diff --git a/src/styles/variables.sass b/app/assets/styles/variables.sass similarity index 100% rename from src/styles/variables.sass rename to app/assets/styles/variables.sass diff --git a/src/components/ArtTag.vue b/app/components/ArtTag.vue similarity index 71% rename from src/components/ArtTag.vue rename to app/components/ArtTag.vue index 3747fc42..8850683c 100644 --- a/src/components/ArtTag.vue +++ b/app/components/ArtTag.vue @@ -1,6 +1,6 @@ @@ -8,6 +8,8 @@ NTag.artwork-tag( diff --git a/src/components/ArtworksList/ArtworksByUser.vue b/app/components/Artwork/ArtworksByUser.vue similarity index 78% rename from src/components/ArtworksList/ArtworksByUser.vue rename to app/components/Artwork/ArtworksByUser.vue index 5652b106..47b1fd07 100644 --- a/src/components/ArtworksList/ArtworksByUser.vue +++ b/app/components/Artwork/ArtworksByUser.vue @@ -19,21 +19,16 @@ diff --git a/src/components/AuthorCard.vue b/app/components/AuthorCard.vue similarity index 71% rename from src/components/AuthorCard.vue rename to app/components/AuthorCard.vue index 59c05392..979454a3 100644 --- a/src/components/AuthorCard.vue +++ b/app/components/AuthorCard.vue @@ -3,12 +3,12 @@ .author-inner(v-if='user') .flex-center .left - RouterLink(:to='"/users/" + user.userId') + NuxtLink(:to='"/users/" + user.userId') img(:src='user.imageBig' alt='') .right .flex h4.plain - RouterLink(:to='"/users/" + user.userId') {{ user.name }} + NuxtLink(:to='"/users/" + user.userId') {{ user.name }} NButton( :loading='loadingUserFollow', :type='user.isFollowed ? "success" : undefined' @@ -16,11 +16,11 @@ round secondary size='small' - v-if='user.userId !== userStore.userId' + v-if='user.userId !== userStore.id' ) template(#icon) - IFasCheck(v-if='user.isFollowed') - IFasPlus(v-else) + ICheck(v-if='user.isFollowed') + IPlus(v-else) | {{ user.isFollowed ? '已关注' : '关注' }} NEllipsis.description.pre(:line-clamp='3', :tooltip='false') {{ user.comment }} ArtworkList.tiny(:list='user.illusts' inline) @@ -35,24 +35,19 @@ diff --git a/src/components/NProgress.vue b/app/components/NProgress.vue similarity index 92% rename from src/components/NProgress.vue rename to app/components/NProgress.vue index 5a9f6bec..9eaab7ab 100644 --- a/src/components/NProgress.vue +++ b/app/components/NProgress.vue @@ -1,4 +1,6 @@ - + diff --git a/src/components/SideNav/SideNav.vue b/app/components/SideNav/Body.vue similarity index 60% rename from src/components/SideNav/SideNav.vue rename to app/components/SideNav/Body.vue index 9247a69c..8cfdf48c 100644 --- a/src/components/SideNav/SideNav.vue +++ b/app/components/SideNav/Body.vue @@ -10,54 +10,44 @@ aside.global-side-nav(:class='{ hidden: !sideNavStore.isOpened }') .group .title 导航 ul - ListLink(link='/' text='首页') - IFasHome.link-icon - ListLink.not-allowed(link='' text='探索发现') - IFasImage.link-icon - ListLink(link='/ranking' text='排行榜') - IFasCrown.link-icon + SideNavListLink(link='/' text='首页') + IHome.svg--SideNavListLink + SideNavListLink.not-allowed(link='' text='插画') + IImage.svg--SideNavListLink + SideNavListLink(link='' text='用户') + IUser.svg--SideNavListLink + SideNavListLink(link='/ranking' text='排行榜') + ICrown.svg--SideNavListLink .group .title 用户 ul - ListLink( + SideNavListLink( :text='userStore.isLoggedIn ? "查看令牌" : "设置令牌"' link='/login' ) - IFasFingerprint.link-icon - ListLink( - :link='userStore.isLoggedIn ? `/users/${userStore.userId}` : `/login?back=${$route.fullPath}`' - text='我的页面' - ) - IFasUser.link-icon - ListLink( - :link='userStore.isLoggedIn ? `/users/${userStore.userId}/following` : `/login?back=${$route.fullPath}`' - text='我的关注' - ) - IFasUser.link-icon - ListLink(link='/following/latest' text='关注用户的作品') - IFasUser.link-icon + IFingerprint.svg--SideNavListLink .group .title PixivNow ul - ListLink(externalLink='https://www.pixiv.net/' text='Pixiv.net') - IFasExternalLinkAlt.link-icon - ListLink(link='/about' text='关于我们') - IFasHeart.link-icon + SideNavListLink( + externalLink='https://www.pixiv.net/' + text='Pixiv.net' + ) + IExternalLinkAlt.svg--SideNavListLink + SideNavListLink(link='/about' text='关于我们') + IHeart.svg--SideNavListLink diff --git a/src/assets/logo.png b/src/assets/logo.png deleted file mode 100644 index f3d2503f..00000000 Binary files a/src/assets/logo.png and /dev/null differ diff --git a/src/components/LazyLoad.vue b/src/components/LazyLoad.vue deleted file mode 100644 index 772fe87a..00000000 --- a/src/components/LazyLoad.vue +++ /dev/null @@ -1,56 +0,0 @@ - - - - - diff --git a/src/components/userData.ts b/src/components/userData.ts deleted file mode 100644 index 840c1337..00000000 --- a/src/components/userData.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { PixivUser } from '@/types' -import Cookies from 'js-cookie' - -export function existsSessionId(): boolean { - const sessionId = Cookies.get('PHPSESSID') - if (sessionId) { - return true - } else { - Cookies.remove('CSRFTOKEN') - return false - } -} - -export async function initUser(): Promise { - try { - const { data } = await axios.get<{ userData: PixivUser; token: string }>( - `/api/user`, - { - headers: { - 'Cache-Control': 'no-store', - }, - } - ) - if (data.token) { - console.log('session ID认证成功', data) - Cookies.set('CSRFTOKEN', data.token, { secure: true, sameSite: 'Strict' }) - const res = data.userData - return res - } else { - Cookies.remove('CSRFTOKEN') - return Promise.reject('无效的session ID') - } - } catch (err) { - Cookies.remove('CSRFTOKEN') - return Promise.reject(err) - } -} - -export function login(token: string): Promise { - if (!validateSessionId(token)) { - console.error('访问令牌格式错误') - return Promise.reject('访问令牌格式错误') - } - Cookies.set('PHPSESSID', token, { - expires: 180, - path: '/', - secure: true, - sameSite: 'Strict', - }) - return initUser() -} - -export function logout(): void { - const token = Cookies.get('PHPSESSID') - if (token && confirm(`您要移除您的令牌吗?\n${token}`)) { - Cookies.remove('PHPSESSID') - Cookies.remove('CSRFTOKEN') - } -} - -export function validateSessionId(token: string): boolean { - return /^\d{2,10}_[0-9A-Za-z]{32}$/.test(token) -} - -export function exampleSessionId(): string { - const uid = new Uint32Array(1) - window.crypto.getRandomValues(uid) - const secret = (() => { - const strSet = - 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' - const final = [] - const indexes = new Uint8Array(32) - window.crypto.getRandomValues(indexes) - for (const i of indexes) { - const charIndex = Math.floor((i * strSet.length) / 256) - final.push(strSet[charIndex]) - } - return final.join('') - })() - return `${uid[0]}_${secret}` -} diff --git a/src/composables/states.ts b/src/composables/states.ts deleted file mode 100644 index 74e9bb46..00000000 --- a/src/composables/states.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { defineStore } from 'pinia' -import { PixivUser } from '@/types' - -export const useSideNavStore = defineStore('sidenav', () => { - const openState = ref(false) - const isOpened = computed(() => openState.value) - function toggle() { - openState.value = !openState.value - } - function open() { - openState.value = true - } - function close() { - openState.value = false - } - return { openState, isOpened, toggle, open, close } -}) - -export const useUserStore = defineStore('user', () => { - const user = ref(null) - const isLoggedIn = computed(() => !!user.value) - const userId = computed(() => user.value?.id) - const userName = computed(() => user.value?.name) - const userPixivId = computed(() => user.value?.pixivId) - const userProfileImg = computed(() => user.value?.profileImg) - const userProfileImgBig = computed(() => user.value?.profileImgBig) - function login(data: PixivUser) { - user.value = data - } - function logout() { - user.value = null - } - return { - user, - isLoggedIn, - userId, - userName, - userPixivId, - userProfileImg, - userProfileImgBig, - login, - logout, - } -}) diff --git a/src/config.ts b/src/config.ts deleted file mode 100644 index 4a7a186d..00000000 --- a/src/config.ts +++ /dev/null @@ -1,23 +0,0 @@ -// Env -import { version } from '../package.json' -export { version } - -export const SITE_ENV = - import.meta.env.MODE === 'development' || - version.includes('-') || - location.hostname === 'pixiv-next.vercel.app' - ? 'development' - : 'production' - -// Copyright links -// Do not modify please -export const GITHUB_OWNER = 'FreeNowOrg' -export const GITHUB_REPO = 'PixivNow' -export const GITHUB_URL = `https://github.com/${GITHUB_OWNER}/${GITHUB_REPO}` - -// Site name -export const PROJECT_NAME = 'PixivNow' -export const PROJECT_TAGLINE = 'Enjoy Pixiv Now (pixiv.js.org)' - -// Image proxy cache seconds -export const IMAGE_CACHE_SECONDS = 12 * 60 * 60 * 1000 diff --git a/src/env.d.ts b/src/env.d.ts deleted file mode 100644 index f86d5a0a..00000000 --- a/src/env.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -/// - -// declare module '*.vue' { -// import { ComponentOptions } from 'vue' -// const componentOptions: ComponentOptions -// export default componentOptions -// } diff --git a/src/main.ts b/src/main.ts deleted file mode 100644 index 3cc4358c..00000000 --- a/src/main.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { createApp } from 'vue' -import { SITE_ENV } from '@/config' -import { registerPlugins } from '@/plugins' -import App from './App.vue' -import '@/styles/index.sass' - -// Create App -const app = createApp(App) - -registerPlugins(app) - -// Mount -app.mount('#app') -document.body?.setAttribute('data-env', SITE_ENV) diff --git a/src/plugins/i18n.ts b/src/plugins/i18n.ts deleted file mode 100644 index 55e63af6..00000000 --- a/src/plugins/i18n.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { createI18n, I18n } from 'vue-i18n' - -export const SUPPORTED_LOCALES = ['zh-Hans'] - -export function setupI18n(options = { locale: 'zh-Hans' }) { - const i18n = createI18n({ ...options, legacy: false }) - setI18nLanguage(i18n, options.locale) - return i18n -} - -export function setI18nLanguage( - i18n: I18n, - locale: string -) { - i18n.global.locale.value = locale - document.querySelector('html')?.setAttribute('lang', locale) -} - -export async function loadLocaleMessages( - i18n: I18n, - locale: string -) { - const messages = await import( - /* webpackChunkName: "locale-[request]" */ `@/locales/${locale}.json` - ) - i18n.global.setLocaleMessage(locale, messages.default) - setI18nLanguage(i18n, locale) - - return nextTick() -} diff --git a/src/plugins/index.ts b/src/plugins/index.ts deleted file mode 100644 index 8e451be0..00000000 --- a/src/plugins/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { router } from './router' -import { loadLocaleMessages, setupI18n } from './i18n' -import { createPinia } from 'pinia' -import VueGtag from 'vue-gtag' -import type { App } from 'vue' - -export async function registerPlugins(app: App) { - const i18n = setupI18n() - const initialLocale = 'zh-Hans' - app.use(i18n) - app.use(router) - app.use(createPinia()) - - if (import.meta.env.VITE_GOOGLE_ANALYTICS_ID) { - app.use( - VueGtag, - { - config: { id: import.meta.env.VITE_GOOGLE_ANALYTICS_ID as string }, - }, - router - ) - } - - await loadLocaleMessages(i18n, initialLocale) -} diff --git a/src/plugins/router.ts b/src/plugins/router.ts deleted file mode 100644 index 965979dc..00000000 --- a/src/plugins/router.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router' -import { createDiscreteApi } from 'naive-ui' -const { message } = createDiscreteApi(['message']) - -const routes: RouteRecordRaw[] = [ - { - path: '/', - name: 'home', - component: () => import('@/view/index.vue'), - }, - { - path: '/artworks/:id', - alias: ['/illust/:id', '/i/:id'], - name: 'artworks', - component: () => import('@/view/artworks.vue'), - }, - { - path: '/following/latest', - alias: ['/bookmark_new_illust'], - name: 'following-latest', - component: () => import('@/view/following-latest.vue'), - }, - { - path: '/users/:id', - name: 'users', - alias: ['/u/:id'], - component: () => import('@/view/users.vue'), - }, - { - path: '/users/:id/following', - name: 'following', - component: () => import('@/view/following.vue'), - }, - { - path: '/search/:keyword', - name: 'search-index-redirect', - redirect: (to) => `/search/${to.params.keyword}/1`, - }, - { - path: '/search/:keyword/:p', - name: 'search', - component: () => import('@/view/search.vue'), - }, - { - path: '/ranking', - name: 'ranking', - component: () => import('@/view/ranking.vue'), - }, - { - path: '/login', - name: 'user-login', - component: () => import('@/view/login.vue'), - }, - { - path: '/about', - name: 'about-us', - component: () => import('@/view/about.vue'), - }, - { - path: '/notifications/2024-04-26', - name: 'notification-2024-04-26', - component: () => import('@/view/notifications/2024-04-26.vue'), - }, - { - path: '/:pathMatch(.*)*', - name: 'not-found', - component: () => import('@/view/404.vue'), - }, -] - -export const router = createRouter({ - history: createWebHistory(), - routes, - scrollBehavior(to, from, savedPosition) { - if (savedPosition) { - return savedPosition - } else { - return { - top: 0, - behavior: 'smooth', - } - } - }, -}) - -router.afterEach(({ name }) => { - document.body.setAttribute('data-route', name as string) - // Fix route when modal opened - document.body.style.overflow = 'visible' -}) - -router.onError((error, to, from) => { - console.log(error, to, from) - message.error(error) -}) - -export default router diff --git a/src/utils/ajax.ts b/src/utils/ajax.ts deleted file mode 100644 index 12ba3acf..00000000 --- a/src/utils/ajax.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { AxiosRequestConfig } from 'axios' -import nprogress from 'nprogress' - -export const ajax = axios.create({ - timeout: 15 * 1000, - headers: { - 'Content-Type': 'application/json', - }, -}) -ajax.interceptors.request.use((config) => { - nprogress.start() - return config -}) -ajax.interceptors.response.use( - (res) => { - nprogress.done() - return res - }, - (err) => { - nprogress.done() - return Promise.reject(err) - } -) - -export const ajaxPostWithFormData = ( - url: string, - data: - | string - | string[][] - | Record - | URLSearchParams - | undefined, - config?: AxiosRequestConfig -) => - ajax.post(url, new URLSearchParams(data).toString(), { - ...config, - headers: { - ...config?.headers, - 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8', - }, - }) diff --git a/src/utils/artworkActions.ts b/src/utils/artworkActions.ts deleted file mode 100644 index f0bdcf11..00000000 --- a/src/utils/artworkActions.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { ajax, ajaxPostWithFormData } from '@/utils/ajax' -import { ArtworkInfo, ArtworkInfoOrAd } from '@/types' - -export function sortArtList( - obj: Record -): T[] { - return Object.values(obj).sort((a, b) => +b.id - +a.id) -} - -export function isArtwork(item: ArtworkInfoOrAd): item is ArtworkInfo { - return Object.keys(item).includes('id') -} - -export async function addBookmark( - illust_id: number | `${number}` -): Promise { - return ( - await ajax.post('/ajax/illusts/bookmarks/add', { - illust_id, - restrict: 0, - comment: '', - tags: [], - }) - ).data -} - -export async function removeBookmark( - bookmark_id: number | `${number}` -): Promise { - return ( - await ajaxPostWithFormData('/ajax/illusts/bookmarks/delete', { - bookmark_id: '' + bookmark_id, - }) - ).data -} diff --git a/src/utils/index.ts b/src/utils/index.ts deleted file mode 100644 index f1db3608..00000000 --- a/src/utils/index.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { ArtworkInfo } from '@/types' - -export * from './artworkActions' -export * from './userActions' - -export const defaultArtwork: ArtworkInfo = { - id: '0', - title: '', - description: '', - createDate: '', - updateDate: '', - illustType: 0, - restrict: 0, - xRestrict: 0, - sl: 0, - userId: '0', - userName: '', - alt: '', - width: 0, - height: 0, - pageCount: 0, - isBookmarkable: false, - bookmarkData: null, - titleCaptionTranslation: { - workTitle: null, - workCaption: null, - }, - isUnlisted: false, - url: '', - tags: [], - profileImageUrl: '', - type: 'illust', - aiType: 1, -} diff --git a/src/utils/setTitle.ts b/src/utils/setTitle.ts deleted file mode 100644 index 4e9f1f16..00000000 --- a/src/utils/setTitle.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { PROJECT_NAME, PROJECT_TAGLINE } from '@/config' - -export function setTitle(...args: (string | number | null | undefined)[]) { - return (document.title = [ - ...args.filter((i) => i !== null && typeof i !== 'undefined'), - `${PROJECT_NAME} - ${PROJECT_TAGLINE}`, - ].join(' | ')) -} diff --git a/src/utils/userActions.ts b/src/utils/userActions.ts deleted file mode 100644 index e3cc58a2..00000000 --- a/src/utils/userActions.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { ajaxPostWithFormData } from '@/utils/ajax' - -export async function addUserFollow( - user_id: number | `${number}` -): Promise { - return ( - await ajaxPostWithFormData(`/bookmark_add.php`, { - mode: 'add', - type: 'user', - user_id: '' + user_id, - tag: '', - restrict: '0', - format: 'json', - }) - ).data -} - -export async function removeUserFollow( - user_id: number | `${number}` -): Promise { - return ( - await ajaxPostWithFormData(`/rpc_group_setting.php`, { - mode: 'del', - type: 'bookuser', - id: '' + user_id, - }) - ).data -} diff --git a/src/view/siteCache.ts b/src/view/siteCache.ts deleted file mode 100644 index af7fdae1..00000000 --- a/src/view/siteCache.ts +++ /dev/null @@ -1,10 +0,0 @@ -const _siteCacheData = new Map() -export function setCache(key: string | number, val: any) { - console.log('setCache', key, val) - _siteCacheData.set(key, val) -} -export function getCache(key: string | number) { - const val = _siteCacheData.get(key) - console.log('getCache', key, val) - return val -} diff --git a/test/illustRecommend.json b/tests/illustRecommend.json similarity index 100% rename from test/illustRecommend.json rename to tests/illustRecommend.json diff --git a/test/userBookmarks.json b/tests/userBookmarks.json similarity index 100% rename from test/userBookmarks.json rename to tests/userBookmarks.json diff --git a/tsconfig.json b/tsconfig.json index 8e350031..8939127f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,37 +1,5 @@ { - "compilerOptions": { - "baseUrl": ".", - "target": "esnext", - "module": "esnext", - "moduleResolution": "node", - "strict": true, - "skipLibCheck": true, - "jsx": "preserve", - "sourceMap": true, - "lib": ["esnext", "dom"], - "plugins": [], - "resolveJsonModule": true, - "allowSyntheticDefaultImports": true, - "isolatedModules": true, - "importHelpers": true, - "paths": { - "@/*": ["src/*"] - }, - "types": ["unplugin-icons/types/vue"] - }, - "include": [ - "api/**/*.ts", - "src/**/*.ts", - "src/**/*.d.ts", - "src/**/*.tsx", - "src/**/*.vue", - "src/**/*.json", - "vite.config.ts", - "auto-imports.d.ts", - "components.d.ts", - "middware.ts" - ], - "exclude": ["**/dist"], + "extends": ["./.nuxt/tsconfig.json"], "vueCompilerOptions": { "plugins": ["@vue/language-plugin-pug"] } diff --git a/vite.config.ts b/vite.config.ts deleted file mode 100644 index 31ab29c1..00000000 --- a/vite.config.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { fileURLToPath } from 'url' -import { defineConfig } from 'vite' -import vue from '@vitejs/plugin-vue' -import AutoImport from 'unplugin-auto-import/vite' -import Icons from 'unplugin-icons/vite' -import IconResolver from 'unplugin-icons/resolver' -import Components from 'unplugin-vue-components/vite' -import { NaiveUiResolver } from 'unplugin-vue-components/resolvers' - -const PROD = process.env.NODE_ENV === 'production' - -export default defineConfig({ - plugins: [ - vue(), - AutoImport({ - dts: true, - imports: [ - 'vue', - 'vue-router', - 'vue-i18n', - '@vueuse/core', - { axios: [['default', 'axios']] }, - ], - resolvers: [ - IconResolver({ - alias: { - fas: 'fa-solid', - }, - }), - ], - dirs: ['src/components/**', 'src/composables', 'src/utils', 'src/types'], - }), - Components({ dts: true, resolvers: [NaiveUiResolver()] }), - Icons({ - scale: 1, - defaultClass: 'svg--inline', - }), - ], - build: {}, - esbuild: { - drop: PROD ? ['console'] : [], - }, - server: { host: true }, - resolve: { - alias: { - '@': fileURLToPath(new URL('./src', import.meta.url)), - }, - extensions: ['.js', '.jsx', '.ts', '.tsx', '.vue', '.json'], - }, -})