mirror of
https://github.com/Kuingsmile/PicList.git
synced 2026-06-02 22:31:49 +08:00
✨ Feature(custom): support script market and share or download scripts
This commit is contained in:
@@ -6,6 +6,7 @@ import { galleryRouter } from '~/events/rpc/routes/gallery'
|
|||||||
import { manageRouter } from '~/events/rpc/routes/manage'
|
import { manageRouter } from '~/events/rpc/routes/manage'
|
||||||
import { picbedRouter } from '~/events/rpc/routes/picbed'
|
import { picbedRouter } from '~/events/rpc/routes/picbed'
|
||||||
import { pluginRouter } from '~/events/rpc/routes/plugin'
|
import { pluginRouter } from '~/events/rpc/routes/plugin'
|
||||||
|
import { scriptMarketplaceRouter } from '~/events/rpc/routes/scriptMarketplace'
|
||||||
import { settingRouter } from '~/events/rpc/routes/setting'
|
import { settingRouter } from '~/events/rpc/routes/setting'
|
||||||
import { systemRouter } from '~/events/rpc/routes/system'
|
import { systemRouter } from '~/events/rpc/routes/system'
|
||||||
import { toolboxRouter } from '~/events/rpc/routes/toolbox'
|
import { toolboxRouter } from '~/events/rpc/routes/toolbox'
|
||||||
@@ -63,6 +64,7 @@ const routes = [
|
|||||||
galleryRouter.routes(),
|
galleryRouter.routes(),
|
||||||
picbedRouter.routes(),
|
picbedRouter.routes(),
|
||||||
pluginRouter.routes(),
|
pluginRouter.routes(),
|
||||||
|
scriptMarketplaceRouter.routes(),
|
||||||
settingRouter.routes(),
|
settingRouter.routes(),
|
||||||
systemRouter.routes(),
|
systemRouter.routes(),
|
||||||
toolboxRouter.routes(),
|
toolboxRouter.routes(),
|
||||||
|
|||||||
554
src/main/events/rpc/routes/scriptMarketplace/index.ts
Normal file
554
src/main/events/rpc/routes/scriptMarketplace/index.ts
Normal file
@@ -0,0 +1,554 @@
|
|||||||
|
import path from 'node:path'
|
||||||
|
|
||||||
|
import { scriptsDir } from '@core/datastore/dirs'
|
||||||
|
import picgo from '@core/picgo'
|
||||||
|
import logger from '@core/picgo/logger'
|
||||||
|
import { shell } from 'electron'
|
||||||
|
import fs from 'fs-extra'
|
||||||
|
|
||||||
|
import { RPCRouter } from '~/events/rpc/router'
|
||||||
|
import { IRPCActionType, IRPCType } from '~/utils/enum'
|
||||||
|
|
||||||
|
const GITHUB_REPO_OWNER = 'Kuingsmile'
|
||||||
|
const GITHUB_REPO_NAME = 'piclist-ScriptsHub'
|
||||||
|
const GITHUB_API_BASE = 'https://api.github.com'
|
||||||
|
const GITHUB_RAW_BASE = 'https://raw.githubusercontent.com'
|
||||||
|
const GITHUB_CLIENT_ID = 'Ov23liLELZPZXhQnpadf'
|
||||||
|
|
||||||
|
interface IScriptMeta {
|
||||||
|
name: string
|
||||||
|
author: string
|
||||||
|
description: string
|
||||||
|
version: string
|
||||||
|
fileName: string
|
||||||
|
category: string
|
||||||
|
downloadUrl: string
|
||||||
|
content?: string
|
||||||
|
createdAt?: string
|
||||||
|
updatedAt?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IGitHubAuthState {
|
||||||
|
accessToken: string | null
|
||||||
|
username: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IDeviceCodeResponse {
|
||||||
|
device_code: string
|
||||||
|
user_code: string
|
||||||
|
verification_uri: string
|
||||||
|
expires_in: number
|
||||||
|
interval: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IDeviceFlowState {
|
||||||
|
deviceCode: string | null
|
||||||
|
userCode: string | null
|
||||||
|
verificationUri: string | null
|
||||||
|
expiresAt: number | null
|
||||||
|
interval: number
|
||||||
|
pollingTimer: NodeJS.Timeout | null
|
||||||
|
}
|
||||||
|
|
||||||
|
const authState: IGitHubAuthState = {
|
||||||
|
accessToken: null,
|
||||||
|
username: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
const deviceFlowState: IDeviceFlowState = {
|
||||||
|
deviceCode: null,
|
||||||
|
userCode: null,
|
||||||
|
verificationUri: null,
|
||||||
|
expiresAt: null,
|
||||||
|
interval: 5,
|
||||||
|
pollingTimer: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadAuthState() {
|
||||||
|
const savedToken = picgo.getConfig<string>('scripts.githubToken')
|
||||||
|
const savedUsername = picgo.getConfig<string>('scripts.githubUsername')
|
||||||
|
if (savedToken && savedUsername) {
|
||||||
|
authState.accessToken = savedToken
|
||||||
|
authState.username = savedUsername
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveAuthState() {
|
||||||
|
picgo.saveConfig({
|
||||||
|
'scripts.githubToken': authState.accessToken,
|
||||||
|
'scripts.githubUsername': authState.username,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearAuthState() {
|
||||||
|
authState.accessToken = null
|
||||||
|
authState.username = null
|
||||||
|
picgo.saveConfig({
|
||||||
|
'scripts.githubToken': null,
|
||||||
|
'scripts.githubUsername': null,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseScriptMeta(content: string, fileName: string, category: string): Partial<IScriptMeta> {
|
||||||
|
const meta: Partial<IScriptMeta> = {
|
||||||
|
fileName,
|
||||||
|
category,
|
||||||
|
}
|
||||||
|
|
||||||
|
const metaRegex = /\/\*\*[\s\S]*?\*\//
|
||||||
|
const metaMatch = content.match(metaRegex)
|
||||||
|
|
||||||
|
if (metaMatch) {
|
||||||
|
const metaBlock = metaMatch[0]
|
||||||
|
|
||||||
|
const nameMatch = metaBlock.match(/@name\s+(.+)/)
|
||||||
|
if (nameMatch) meta.name = nameMatch[1].trim()
|
||||||
|
|
||||||
|
const authorMatch = metaBlock.match(/@author\s+(.+)/)
|
||||||
|
if (authorMatch) meta.author = authorMatch[1].trim()
|
||||||
|
|
||||||
|
const descMatch = metaBlock.match(/@description\s+(.+)/)
|
||||||
|
if (descMatch) meta.description = descMatch[1].trim()
|
||||||
|
|
||||||
|
const versionMatch = metaBlock.match(/@version\s+(.+)/)
|
||||||
|
if (versionMatch) meta.version = versionMatch[1].trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!meta.name) meta.name = fileName.replace('.js', '')
|
||||||
|
if (!meta.author) meta.author = 'Unknown'
|
||||||
|
if (!meta.description) meta.description = ''
|
||||||
|
if (!meta.version) meta.version = '1.0.0'
|
||||||
|
|
||||||
|
return meta
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchScriptsList(): Promise<IScriptMeta[]> {
|
||||||
|
const scripts: IScriptMeta[] = []
|
||||||
|
|
||||||
|
try {
|
||||||
|
const treeResponse = await fetch(
|
||||||
|
`${GITHUB_API_BASE}/repos/${GITHUB_REPO_OWNER}/${GITHUB_REPO_NAME}/git/trees/main?recursive=1`,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!treeResponse.ok) {
|
||||||
|
throw new Error(`Failed to fetch repository tree: ${treeResponse.statusText}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const treeData = (await treeResponse.json()) as { tree: { path: string; type: string }[] }
|
||||||
|
|
||||||
|
const jsFiles = treeData.tree.filter(
|
||||||
|
(item: { path: string; type: string }) => item.type === 'blob' && item.path.endsWith('.js'),
|
||||||
|
)
|
||||||
|
|
||||||
|
for (const file of jsFiles) {
|
||||||
|
const pathParts = file.path.split('/')
|
||||||
|
if (pathParts.length >= 2) {
|
||||||
|
const category = pathParts.slice(0, -1).join('.')
|
||||||
|
const fileName = pathParts[pathParts.length - 1]
|
||||||
|
|
||||||
|
const contentResponse = await fetch(
|
||||||
|
`${GITHUB_RAW_BASE}/${GITHUB_REPO_OWNER}/${GITHUB_REPO_NAME}/main/${file.path}`,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (contentResponse.ok) {
|
||||||
|
const content = await contentResponse.text()
|
||||||
|
const meta = parseScriptMeta(content, fileName, category)
|
||||||
|
|
||||||
|
scripts.push({
|
||||||
|
name: meta.name || fileName,
|
||||||
|
author: meta.author || 'Unknown',
|
||||||
|
description: meta.description || '',
|
||||||
|
version: meta.version || '1.0.0',
|
||||||
|
fileName,
|
||||||
|
category,
|
||||||
|
content,
|
||||||
|
downloadUrl: `${GITHUB_RAW_BASE}/${GITHUB_REPO_OWNER}/${GITHUB_REPO_NAME}/main/${file.path}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to fetch scripts list: ${error}`)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
return scripts
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadScript(scriptMeta: IScriptMeta): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(scriptMeta.downloadUrl)
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to download script: ${response.statusText}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = await response.text()
|
||||||
|
|
||||||
|
const categoryParts = scriptMeta.category.split('.')
|
||||||
|
const localPath = path.join(scriptsDir(), ...categoryParts, scriptMeta.fileName)
|
||||||
|
|
||||||
|
fs.ensureDirSync(path.dirname(localPath))
|
||||||
|
|
||||||
|
fs.writeFileSync(localPath, content, 'utf-8')
|
||||||
|
|
||||||
|
logger.info(`Downloaded script ${scriptMeta.name} to ${localPath}`)
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to download script: ${error}`)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function initiateGitHubDeviceFlow(): Promise<{
|
||||||
|
userCode: string
|
||||||
|
verificationUri: string
|
||||||
|
expiresIn: number
|
||||||
|
} | null> {
|
||||||
|
try {
|
||||||
|
if (deviceFlowState.pollingTimer) {
|
||||||
|
clearInterval(deviceFlowState.pollingTimer)
|
||||||
|
deviceFlowState.pollingTimer = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch('https://github.com/login/device/code', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
client_id: GITHUB_CLIENT_ID,
|
||||||
|
scope: 'public_repo',
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to get device code: ${response.statusText}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = (await response.json()) as IDeviceCodeResponse
|
||||||
|
|
||||||
|
deviceFlowState.deviceCode = data.device_code
|
||||||
|
deviceFlowState.userCode = data.user_code
|
||||||
|
deviceFlowState.verificationUri = data.verification_uri
|
||||||
|
deviceFlowState.expiresAt = Date.now() + data.expires_in * 1000
|
||||||
|
deviceFlowState.interval = data.interval || 5
|
||||||
|
|
||||||
|
shell.openExternal(data.verification_uri)
|
||||||
|
|
||||||
|
logger.info(`GitHub Device Flow initiated. User code: ${data.user_code}`)
|
||||||
|
|
||||||
|
return {
|
||||||
|
userCode: data.user_code,
|
||||||
|
verificationUri: data.verification_uri,
|
||||||
|
expiresIn: data.expires_in,
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to initiate GitHub Device Flow: ${error}`)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pollDeviceFlowAuthorization(): Promise<{
|
||||||
|
success: boolean
|
||||||
|
username?: string
|
||||||
|
error?: string
|
||||||
|
nextInterval?: number
|
||||||
|
}> {
|
||||||
|
if (!deviceFlowState.deviceCode) {
|
||||||
|
return { success: false, error: 'No device code available. Please initiate login first.' }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deviceFlowState.expiresAt && Date.now() > deviceFlowState.expiresAt) {
|
||||||
|
return { success: false, error: 'Device code expired. Please try again.' }
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('https://github.com/login/oauth/access_token', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
client_id: GITHUB_CLIENT_ID,
|
||||||
|
device_code: deviceFlowState.deviceCode,
|
||||||
|
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
console.log('Poll response status:', response.status)
|
||||||
|
|
||||||
|
const data = (await response.json()) as {
|
||||||
|
access_token?: string
|
||||||
|
error?: string
|
||||||
|
error_description?: string
|
||||||
|
interval?: number
|
||||||
|
}
|
||||||
|
console.log('Poll response data:', data)
|
||||||
|
|
||||||
|
if (data.error) {
|
||||||
|
if (data.error === 'authorization_pending') {
|
||||||
|
return { success: false, error: 'authorization_pending' }
|
||||||
|
} else if (data.error === 'slow_down') {
|
||||||
|
return { success: false, error: 'slow_down', nextInterval: data.interval || deviceFlowState.interval + 5 }
|
||||||
|
} else if (data.error === 'expired_token') {
|
||||||
|
return { success: false, error: 'Device code expired. Please try again.' }
|
||||||
|
} else if (data.error === 'access_denied') {
|
||||||
|
return { success: false, error: 'Authorization denied by user.' }
|
||||||
|
} else {
|
||||||
|
return { success: false, error: data.error_description || data.error }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.access_token) {
|
||||||
|
authState.accessToken = data.access_token
|
||||||
|
const userResponse = await fetch(`${GITHUB_API_BASE}/user`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${authState.accessToken}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (userResponse.ok) {
|
||||||
|
const userData = (await userResponse.json()) as { login: string }
|
||||||
|
authState.username = userData.login
|
||||||
|
}
|
||||||
|
|
||||||
|
saveAuthState()
|
||||||
|
|
||||||
|
deviceFlowState.deviceCode = null
|
||||||
|
deviceFlowState.userCode = null
|
||||||
|
deviceFlowState.verificationUri = null
|
||||||
|
deviceFlowState.expiresAt = null
|
||||||
|
|
||||||
|
logger.info(`GitHub Device Flow completed. User: ${authState.username}`)
|
||||||
|
|
||||||
|
return { success: true, username: authState.username || undefined }
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: false, error: 'Unknown error during authorization' }
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to poll device flow authorization: ${error}`)
|
||||||
|
return { success: false, error: String(error) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelDeviceFlow() {
|
||||||
|
if (deviceFlowState.pollingTimer) {
|
||||||
|
clearInterval(deviceFlowState.pollingTimer)
|
||||||
|
deviceFlowState.pollingTimer = null
|
||||||
|
}
|
||||||
|
deviceFlowState.deviceCode = null
|
||||||
|
deviceFlowState.userCode = null
|
||||||
|
deviceFlowState.verificationUri = null
|
||||||
|
deviceFlowState.expiresAt = null
|
||||||
|
}
|
||||||
|
|
||||||
|
async function shareScript(
|
||||||
|
scriptPath: string[],
|
||||||
|
metadata: { name: string; author: string; description: string },
|
||||||
|
): Promise<{ success: boolean; prUrl?: string; error?: string }> {
|
||||||
|
if (!authState.accessToken) {
|
||||||
|
return { success: false, error: 'Not authenticated with GitHub' }
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const localPath = path.join(scriptsDir(), ...scriptPath)
|
||||||
|
if (!fs.existsSync(localPath)) {
|
||||||
|
return { success: false, error: 'Script file not found' }
|
||||||
|
}
|
||||||
|
|
||||||
|
let content = fs.readFileSync(localPath, 'utf-8')
|
||||||
|
|
||||||
|
const metaHeader = `/**
|
||||||
|
* @name ${metadata.name}
|
||||||
|
* @author ${metadata.author}
|
||||||
|
* @description ${metadata.description}
|
||||||
|
* @version 1.0.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
`
|
||||||
|
|
||||||
|
if (!content.startsWith('/**')) {
|
||||||
|
content = metaHeader + content
|
||||||
|
}
|
||||||
|
|
||||||
|
const category = scriptPath.length > 1 ? scriptPath.slice(0, -1).join('/') : 'manualTrigger'
|
||||||
|
const fileName = `${metadata.author}-${scriptPath[scriptPath.length - 1]}`
|
||||||
|
const filePath = `${category}/${fileName}`
|
||||||
|
|
||||||
|
const forkResponse = await fetch(`${GITHUB_API_BASE}/repos/${GITHUB_REPO_OWNER}/${GITHUB_REPO_NAME}/forks`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${authState.accessToken}`,
|
||||||
|
Accept: 'application/vnd.github.v3+json',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!forkResponse.ok && forkResponse.status !== 202) {
|
||||||
|
const forkError = await forkResponse.text()
|
||||||
|
logger.error(`Fork response: ${forkError}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2000))
|
||||||
|
|
||||||
|
const mainBranchResponse = await fetch(
|
||||||
|
`${GITHUB_API_BASE}/repos/${authState.username}/${GITHUB_REPO_NAME}/git/ref/heads/main`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${authState.accessToken}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
let baseSha: string
|
||||||
|
if (mainBranchResponse.ok) {
|
||||||
|
const mainBranchData = (await mainBranchResponse.json()) as { object: { sha: string } }
|
||||||
|
baseSha = mainBranchData.object.sha
|
||||||
|
} else {
|
||||||
|
const origBranchResponse = await fetch(
|
||||||
|
`${GITHUB_API_BASE}/repos/${GITHUB_REPO_OWNER}/${GITHUB_REPO_NAME}/git/ref/heads/main`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${authState.accessToken}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
const origBranchData = (await origBranchResponse.json()) as { object: { sha: string } }
|
||||||
|
baseSha = origBranchData.object.sha
|
||||||
|
}
|
||||||
|
|
||||||
|
const branchName = `add-script-${Date.now()}`
|
||||||
|
await fetch(`${GITHUB_API_BASE}/repos/${authState.username}/${GITHUB_REPO_NAME}/git/refs`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${authState.accessToken}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
ref: `refs/heads/${branchName}`,
|
||||||
|
sha: baseSha,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
const fileResponse = await fetch(
|
||||||
|
`${GITHUB_API_BASE}/repos/${authState.username}/${GITHUB_REPO_NAME}/contents/${filePath}`,
|
||||||
|
{
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${authState.accessToken}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
message: `Add script: ${metadata.name}`,
|
||||||
|
content: Buffer.from(content).toString('base64'),
|
||||||
|
branch: branchName,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!fileResponse.ok) {
|
||||||
|
const errorData = await fileResponse.text()
|
||||||
|
return { success: false, error: `Failed to create file: ${errorData}` }
|
||||||
|
}
|
||||||
|
|
||||||
|
const prResponse = await fetch(`${GITHUB_API_BASE}/repos/${GITHUB_REPO_OWNER}/${GITHUB_REPO_NAME}/pulls`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${authState.accessToken}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
title: `Add script: ${metadata.name}`,
|
||||||
|
body: `## Script Information\n\n- **Name**: ${metadata.name}\n- **Author**: ${metadata.author}\n- **Description**: ${metadata.description}\n\nSubmitted via PicList Script Marketplace`,
|
||||||
|
head: `${authState.username}:${branchName}`,
|
||||||
|
base: 'main',
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!prResponse.ok) {
|
||||||
|
const prError = await prResponse.text()
|
||||||
|
return { success: false, error: `Failed to create PR: ${prError}` }
|
||||||
|
}
|
||||||
|
|
||||||
|
const prData = (await prResponse.json()) as { html_url: string }
|
||||||
|
return { success: true, prUrl: prData.html_url }
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to share script: ${error}`)
|
||||||
|
return { success: false, error: String(error) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadAuthState()
|
||||||
|
|
||||||
|
const scriptMarketplaceRouter = new RPCRouter()
|
||||||
|
|
||||||
|
const scriptMarketplaceRoutes = [
|
||||||
|
{
|
||||||
|
action: IRPCActionType.SCRIPT_MARKETPLACE_FETCH_LIST,
|
||||||
|
handler: async () => {
|
||||||
|
return await fetchScriptsList()
|
||||||
|
},
|
||||||
|
type: IRPCType.INVOKE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: IRPCActionType.SCRIPT_MARKETPLACE_DOWNLOAD,
|
||||||
|
handler: async (_: IIPCEvent, args: [IScriptMeta]) => {
|
||||||
|
return await downloadScript(args[0])
|
||||||
|
},
|
||||||
|
type: IRPCType.INVOKE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: IRPCActionType.SCRIPT_MARKETPLACE_SHARE,
|
||||||
|
handler: async (_: IIPCEvent, args: [string[], { name: string; author: string; description: string }]) => {
|
||||||
|
return await shareScript(args[0], args[1])
|
||||||
|
},
|
||||||
|
type: IRPCType.INVOKE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: IRPCActionType.SCRIPT_MARKETPLACE_CHECK_GITHUB_AUTH,
|
||||||
|
handler: async () => {
|
||||||
|
await loadAuthState()
|
||||||
|
return {
|
||||||
|
isAuthenticated: !!authState.accessToken,
|
||||||
|
username: authState.username,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type: IRPCType.INVOKE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: IRPCActionType.SCRIPT_MARKETPLACE_GITHUB_LOGIN,
|
||||||
|
handler: async () => {
|
||||||
|
return await initiateGitHubDeviceFlow()
|
||||||
|
},
|
||||||
|
type: IRPCType.INVOKE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: IRPCActionType.SCRIPT_MARKETPLACE_GITHUB_POLL,
|
||||||
|
handler: async () => {
|
||||||
|
return await pollDeviceFlowAuthorization()
|
||||||
|
},
|
||||||
|
type: IRPCType.INVOKE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: IRPCActionType.SCRIPT_MARKETPLACE_GITHUB_CANCEL,
|
||||||
|
handler: async () => {
|
||||||
|
cancelDeviceFlow()
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
type: IRPCType.INVOKE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: IRPCActionType.SCRIPT_MARKETPLACE_GITHUB_LOGOUT,
|
||||||
|
handler: async () => {
|
||||||
|
clearAuthState()
|
||||||
|
cancelDeviceFlow()
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
type: IRPCType.INVOKE,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
scriptMarketplaceRouter.addBatch(scriptMarketplaceRoutes)
|
||||||
|
|
||||||
|
export { scriptMarketplaceRouter }
|
||||||
@@ -140,6 +140,16 @@ export const IRPCActionType = {
|
|||||||
DELETE_SCRIPTS_FILE: 'DELETE_SCRIPTS_FILE',
|
DELETE_SCRIPTS_FILE: 'DELETE_SCRIPTS_FILE',
|
||||||
RUN_SCRIPT_FILE: 'RUN_SCRIPT_FILE',
|
RUN_SCRIPT_FILE: 'RUN_SCRIPT_FILE',
|
||||||
|
|
||||||
|
// script marketplace rpc
|
||||||
|
SCRIPT_MARKETPLACE_FETCH_LIST: 'SCRIPT_MARKETPLACE_FETCH_LIST',
|
||||||
|
SCRIPT_MARKETPLACE_DOWNLOAD: 'SCRIPT_MARKETPLACE_DOWNLOAD',
|
||||||
|
SCRIPT_MARKETPLACE_SHARE: 'SCRIPT_MARKETPLACE_SHARE',
|
||||||
|
SCRIPT_MARKETPLACE_CHECK_GITHUB_AUTH: 'SCRIPT_MARKETPLACE_CHECK_GITHUB_AUTH',
|
||||||
|
SCRIPT_MARKETPLACE_GITHUB_LOGIN: 'SCRIPT_MARKETPLACE_GITHUB_LOGIN',
|
||||||
|
SCRIPT_MARKETPLACE_GITHUB_POLL: 'SCRIPT_MARKETPLACE_GITHUB_POLL',
|
||||||
|
SCRIPT_MARKETPLACE_GITHUB_CANCEL: 'SCRIPT_MARKETPLACE_GITHUB_CANCEL',
|
||||||
|
SCRIPT_MARKETPLACE_GITHUB_LOGOUT: 'SCRIPT_MARKETPLACE_GITHUB_LOGOUT',
|
||||||
|
|
||||||
// shortkey setting rpc
|
// shortkey setting rpc
|
||||||
SHORTKEY_UPDATE: 'SHORTKEY_UPDATE',
|
SHORTKEY_UPDATE: 'SHORTKEY_UPDATE',
|
||||||
SHORTKEY_BIND_OR_UNBIND: 'SHORTKEY_BIND_OR_UNBIND',
|
SHORTKEY_BIND_OR_UNBIND: 'SHORTKEY_BIND_OR_UNBIND',
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue'
|
|||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: { type: String, default: '' },
|
modelValue: { type: String, default: '' },
|
||||||
language: { type: String, default: 'javascript' },
|
language: { type: String, default: 'javascript' },
|
||||||
|
readOnly: { type: Boolean, default: false },
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['update:modelValue'])
|
const emit = defineEmits(['update:modelValue'])
|
||||||
@@ -35,6 +36,7 @@ onMounted(() => {
|
|||||||
search({ top: true }),
|
search({ top: true }),
|
||||||
keymap.of([...searchKeymap]),
|
keymap.of([...searchKeymap]),
|
||||||
EditorView.lineWrapping,
|
EditorView.lineWrapping,
|
||||||
|
EditorView.editable.of(!props.readOnly),
|
||||||
EditorView.updateListener.of(update => {
|
EditorView.updateListener.of(update => {
|
||||||
if (update.docChanged) {
|
if (update.docChanged) {
|
||||||
emit('update:modelValue', update.state.doc.toString())
|
emit('update:modelValue', update.state.doc.toString())
|
||||||
|
|||||||
@@ -736,6 +736,55 @@
|
|||||||
"emptyScriptList": "Script list is empty",
|
"emptyScriptList": "Script list is empty",
|
||||||
"enabled": "Enabled",
|
"enabled": "Enabled",
|
||||||
"enableScript": "Enable Script",
|
"enableScript": "Enable Script",
|
||||||
|
"marketplace": {
|
||||||
|
"allCategories": "All Categories",
|
||||||
|
"author": "Author",
|
||||||
|
"browseMarketplace": "Browse Marketplace",
|
||||||
|
"category": "Category",
|
||||||
|
"codeCopied": "Code copied to clipboard",
|
||||||
|
"copyCode": "Copy code",
|
||||||
|
"description": "Browse and download community scripts",
|
||||||
|
"deviceCodeExpired": "Device code expired. Please try again.",
|
||||||
|
"deviceFlowInstructions": "A browser window has opened. Enter the code below on GitHub to authorize PicList.",
|
||||||
|
"download": "Download",
|
||||||
|
"downloaded": "Downloaded",
|
||||||
|
"downloadFailed": "Failed to download script",
|
||||||
|
"downloading": "Downloading...",
|
||||||
|
"downloadSuccess": "Script downloaded successfully",
|
||||||
|
"filterByCategory": "Filter by category",
|
||||||
|
"loadFailed": "Failed to load marketplace scripts",
|
||||||
|
"loadingScripts": "Loading scripts...",
|
||||||
|
"loggedInAs": "Logged in as {username}",
|
||||||
|
"loginFailed": "GitHub login failed",
|
||||||
|
"loginRequired": "Please login with GitHub to share scripts",
|
||||||
|
"loginSuccess": "GitHub login successful",
|
||||||
|
"loginWithGitHub": "Login with GitHub",
|
||||||
|
"logout": "Logout",
|
||||||
|
"logoutSuccess": "Logged out successfully",
|
||||||
|
"metadataRequired": "Please fill in all metadata fields",
|
||||||
|
"noScriptsFound": "No scripts found in marketplace",
|
||||||
|
"openMarketplaceRepo": "Open Marketplace Repository",
|
||||||
|
"prCreated": "Pull request created successfully",
|
||||||
|
"retry": "Retry",
|
||||||
|
"scriptAuthor": "Author Name",
|
||||||
|
"scriptContent": "Script Content",
|
||||||
|
"scriptDescription": "Description",
|
||||||
|
"scriptMetadata": "Script Metadata",
|
||||||
|
"scriptName": "Script Name",
|
||||||
|
"scriptVersion": "脚本版本",
|
||||||
|
"searchPlaceholder": "Search scripts...",
|
||||||
|
"share": "Share Script",
|
||||||
|
"shareFailed": "Failed to share script",
|
||||||
|
"shareScript": "Share to Marketplace",
|
||||||
|
"shareSuccess": "Script shared successfully! PR created.",
|
||||||
|
"sharing": "Sharing...",
|
||||||
|
"showScriptCode": "Show Code",
|
||||||
|
"title": "Script Marketplace",
|
||||||
|
"version": "Version",
|
||||||
|
"viewPR": "View Pull Request",
|
||||||
|
"waitingForAuth": "Waiting for authorization...",
|
||||||
|
"yourCode": "Your verification code:"
|
||||||
|
},
|
||||||
"newScript": "New Script",
|
"newScript": "New Script",
|
||||||
"NoScripts": "No Scripts",
|
"NoScripts": "No Scripts",
|
||||||
"noScriptsFound": "No Scripts Found",
|
"noScriptsFound": "No Scripts Found",
|
||||||
|
|||||||
@@ -736,6 +736,55 @@
|
|||||||
"emptyScriptList": "脚本列表为空",
|
"emptyScriptList": "脚本列表为空",
|
||||||
"enabled": "已启用",
|
"enabled": "已启用",
|
||||||
"enableScript": "启用脚本",
|
"enableScript": "启用脚本",
|
||||||
|
"marketplace": {
|
||||||
|
"allCategories": "全部分类",
|
||||||
|
"author": "作者",
|
||||||
|
"browseMarketplace": "浏览市场",
|
||||||
|
"category": "分类",
|
||||||
|
"codeCopied": "代码已复制到剪贴板",
|
||||||
|
"copyCode": "复制代码",
|
||||||
|
"description": "浏览和下载社区脚本",
|
||||||
|
"deviceCodeExpired": "设备代码已过期,请重试。",
|
||||||
|
"deviceFlowInstructions": "浏览器窗口已打开。请在 GitHub 上输入以下代码以授权 PicList。",
|
||||||
|
"download": "下载",
|
||||||
|
"downloaded": "已下载",
|
||||||
|
"downloadFailed": "脚本下载失败",
|
||||||
|
"downloading": "下载中...",
|
||||||
|
"downloadSuccess": "脚本下载成功",
|
||||||
|
"filterByCategory": "按分类筛选",
|
||||||
|
"loadFailed": "加载市场脚本失败",
|
||||||
|
"loadingScripts": "正在加载脚本...",
|
||||||
|
"loggedInAs": "已登录为 {username}",
|
||||||
|
"loginFailed": "GitHub 登录失败",
|
||||||
|
"loginRequired": "请使用 GitHub 登录以分享脚本",
|
||||||
|
"loginSuccess": "GitHub 登录成功",
|
||||||
|
"loginWithGitHub": "使用 GitHub 登录",
|
||||||
|
"logout": "退出登录",
|
||||||
|
"logoutSuccess": "已退出登录",
|
||||||
|
"metadataRequired": "请填写所有元数据字段",
|
||||||
|
"noScriptsFound": "市场中暂无脚本",
|
||||||
|
"openMarketplaceRepo": "打开市场仓库",
|
||||||
|
"prCreated": "Pull Request 创建成功",
|
||||||
|
"retry": "重试",
|
||||||
|
"scriptAuthor": "作者名称",
|
||||||
|
"scriptContent": "脚本内容",
|
||||||
|
"scriptDescription": "描述",
|
||||||
|
"scriptMetadata": "脚本元数据",
|
||||||
|
"scriptName": "脚本名称",
|
||||||
|
"scriptVersion": "脚本版本",
|
||||||
|
"searchPlaceholder": "搜索脚本...",
|
||||||
|
"share": "分享脚本",
|
||||||
|
"shareFailed": "脚本分享失败",
|
||||||
|
"shareScript": "分享到市场",
|
||||||
|
"shareSuccess": "脚本分享成功!已创建 PR。",
|
||||||
|
"sharing": "分享中...",
|
||||||
|
"showScriptCode": "显示代码",
|
||||||
|
"title": "脚本市场",
|
||||||
|
"version": "版本",
|
||||||
|
"viewPR": "查看 Pull Request",
|
||||||
|
"waitingForAuth": "等待授权中...",
|
||||||
|
"yourCode": "您的验证码:"
|
||||||
|
},
|
||||||
"newScript": "新脚本",
|
"newScript": "新脚本",
|
||||||
"NoScripts": "暂无脚本",
|
"NoScripts": "暂无脚本",
|
||||||
"noScriptsFound": "未找到脚本",
|
"noScriptsFound": "未找到脚本",
|
||||||
|
|||||||
@@ -736,6 +736,55 @@
|
|||||||
"emptyScriptList": "腳本列表為空",
|
"emptyScriptList": "腳本列表為空",
|
||||||
"enabled": "已啟用",
|
"enabled": "已啟用",
|
||||||
"enableScript": "啟用腳本",
|
"enableScript": "啟用腳本",
|
||||||
|
"marketplace": {
|
||||||
|
"allCategories": "全部分類",
|
||||||
|
"author": "作者",
|
||||||
|
"browseMarketplace": "瀏覽市場",
|
||||||
|
"category": "分類",
|
||||||
|
"codeCopied": "代碼已複製到剪貼板",
|
||||||
|
"copyCode": "複製代碼",
|
||||||
|
"description": "瀏覽和下載社區腳本",
|
||||||
|
"deviceCodeExpired": "設備代碼已過期,請重試。",
|
||||||
|
"deviceFlowInstructions": "瀏覽器視窗已開啟。請在 GitHub 上輸入以下代碼以授權 PicList。",
|
||||||
|
"download": "下載",
|
||||||
|
"downloaded": "已下載",
|
||||||
|
"downloadFailed": "腳本下載失敗",
|
||||||
|
"downloading": "下載中...",
|
||||||
|
"downloadSuccess": "腳本下載成功",
|
||||||
|
"filterByCategory": "按分類篩選",
|
||||||
|
"loadFailed": "加載市場腳本失敗",
|
||||||
|
"loadingScripts": "正在加載腳本...",
|
||||||
|
"loggedInAs": "已登錄為 {username}",
|
||||||
|
"loginFailed": "GitHub 登錄失敗",
|
||||||
|
"loginRequired": "請使用 GitHub 登錄以分享腳本",
|
||||||
|
"loginSuccess": "GitHub 登錄成功",
|
||||||
|
"loginWithGitHub": "使用 GitHub 登錄",
|
||||||
|
"logout": "退出登錄",
|
||||||
|
"logoutSuccess": "已退出登錄",
|
||||||
|
"metadataRequired": "請填寫所有元數據字段",
|
||||||
|
"noScriptsFound": "市場中暫無腳本",
|
||||||
|
"openMarketplaceRepo": "打開市場倉庫",
|
||||||
|
"prCreated": "Pull Request 創建成功",
|
||||||
|
"retry": "重試",
|
||||||
|
"scriptAuthor": "作者名稱",
|
||||||
|
"scriptContent": "腳本內容",
|
||||||
|
"scriptDescription": "描述",
|
||||||
|
"scriptMetadata": "腳本元數據",
|
||||||
|
"scriptName": "腳本名稱",
|
||||||
|
"scriptVersion": "脚本版本",
|
||||||
|
"searchPlaceholder": "搜索腳本...",
|
||||||
|
"share": "分享腳本",
|
||||||
|
"shareFailed": "腳本分享失敗",
|
||||||
|
"shareScript": "分享到市場",
|
||||||
|
"shareSuccess": "腳本分享成功!已創建 PR。",
|
||||||
|
"sharing": "分享中...",
|
||||||
|
"showScriptCode": "顯示代碼",
|
||||||
|
"title": "腳本市場",
|
||||||
|
"version": "版本",
|
||||||
|
"viewPR": "查看 Pull Request",
|
||||||
|
"waitingForAuth": "等待授權中...",
|
||||||
|
"yourCode": "您的驗證碼:"
|
||||||
|
},
|
||||||
"newScript": "新腳本",
|
"newScript": "新腳本",
|
||||||
"NoScripts": "暫無腳本",
|
"NoScripts": "暫無腳本",
|
||||||
"noScriptsFound": "未找到腳本",
|
"noScriptsFound": "未找到腳本",
|
||||||
|
|||||||
@@ -12,6 +12,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-wrap gap-3 overflow-visible">
|
<div class="flex flex-wrap gap-3 overflow-visible">
|
||||||
|
<CustomButton
|
||||||
|
type="secondary"
|
||||||
|
:icon="StoreIcon"
|
||||||
|
:text="t('pages.scripts.marketplace.browseMarketplace')"
|
||||||
|
@click="openMarketplace"
|
||||||
|
/>
|
||||||
<CustomButton
|
<CustomButton
|
||||||
type="primary"
|
type="primary"
|
||||||
:icon="FolderOpen"
|
:icon="FolderOpen"
|
||||||
@@ -83,6 +89,13 @@
|
|||||||
>
|
>
|
||||||
<Trash2 :size="14" />
|
<Trash2 :size="14" />
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
class="action-btn bg-accent text-white!"
|
||||||
|
:title="t('pages.scripts.marketplace.shareScript')"
|
||||||
|
@click.stop="openShareDialog(item)"
|
||||||
|
>
|
||||||
|
<Share2Icon :size="14" />
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="item.category === 'manualTrigger'"
|
v-if="item.category === 'manualTrigger'"
|
||||||
class="action-btn bg-accent/50 text-white!"
|
class="action-btn bg-accent/50 text-white!"
|
||||||
@@ -207,6 +220,304 @@
|
|||||||
<CustomButton type="primary" :text="t('common.confirm')" @click="handleNewScriptNameConfirm" />
|
<CustomButton type="primary" :text="t('common.confirm')" @click="handleNewScriptNameConfirm" />
|
||||||
</template>
|
</template>
|
||||||
</CustomModal>
|
</CustomModal>
|
||||||
|
|
||||||
|
<CustomModal
|
||||||
|
v-if="marketplaceVisible"
|
||||||
|
v-model:visible="marketplaceVisible"
|
||||||
|
:title="t('pages.scripts.marketplace.title')"
|
||||||
|
>
|
||||||
|
<div class="flex h-full w-full flex-col gap-4 p-4">
|
||||||
|
<div class="flex flex-wrap items-center justify-between gap-4">
|
||||||
|
<div class="relative flex flex-1 items-center">
|
||||||
|
<SearchIcon class="absolute left-3 z-1 text-secondary" :size="18" />
|
||||||
|
<input
|
||||||
|
v-model="marketplaceSearch"
|
||||||
|
type="text"
|
||||||
|
class="w-full rounded-lg border border-border bg-bg-secondary px-10 py-2 text-sm text-main placeholder:text-secondary focus:border-accent focus:outline-none"
|
||||||
|
:placeholder="t('pages.scripts.marketplace.searchPlaceholder')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<template v-if="githubAuth.isAuthenticated">
|
||||||
|
<span class="text-sm text-secondary">
|
||||||
|
{{ t('pages.scripts.marketplace.loggedInAs', { username: githubAuth.username }) }}
|
||||||
|
</span>
|
||||||
|
<CustomButton
|
||||||
|
type="secondary"
|
||||||
|
:icon="LogOutIcon"
|
||||||
|
:text="t('pages.scripts.marketplace.logout')"
|
||||||
|
@click="handleGitHubLogout"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<CustomButton
|
||||||
|
type="secondary"
|
||||||
|
:icon="null"
|
||||||
|
:text="t('pages.scripts.marketplace.loginWithGitHub')"
|
||||||
|
@click="handleGitHubLogin"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<svg width="18" height="18" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<title>GitHub</title>
|
||||||
|
<path
|
||||||
|
d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
</CustomButton>
|
||||||
|
</template>
|
||||||
|
<CustomButton
|
||||||
|
type="secondary"
|
||||||
|
:icon="ExternalLinkIcon"
|
||||||
|
:text="t('pages.scripts.marketplace.openMarketplaceRepo')"
|
||||||
|
@click="openMarketplaceRepo"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<MultiSelect
|
||||||
|
v-model:choosed="marketplaceCategoryFilter"
|
||||||
|
:zero-placeholder="t('pages.scripts.marketplace.allCategories')"
|
||||||
|
:all-list="supportedScriptCategories"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="marketplaceLoading" class="flex flex-1 flex-col items-center justify-center gap-4">
|
||||||
|
<div class="h-10 w-10 animate-spin rounded-full border-4 border-border border-t-accent" />
|
||||||
|
<span class="text-sm text-secondary">{{ t('pages.scripts.marketplace.loadingScripts') }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="marketplaceError" class="flex flex-1 flex-col items-center justify-center gap-4">
|
||||||
|
<XCircleIcon :size="48" class="text-danger" />
|
||||||
|
<span class="text-sm text-danger">{{ t('pages.scripts.marketplace.loadFailed') }}</span>
|
||||||
|
<CustomButton
|
||||||
|
type="primary"
|
||||||
|
:icon="RefreshCwIcon"
|
||||||
|
:text="t('pages.scripts.marketplace.retry')"
|
||||||
|
@click="fetchMarketplaceScripts"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="flex-1 overflow-hidden rounded-lg border border-border">
|
||||||
|
<div class="no-scrollbar h-full overflow-auto p-4">
|
||||||
|
<div
|
||||||
|
v-if="filteredMarketplaceScripts.length === 0"
|
||||||
|
class="flex h-full flex-col items-center justify-center gap-4"
|
||||||
|
>
|
||||||
|
<PackageIcon :size="48" class="text-secondary opacity-50" />
|
||||||
|
<span class="text-sm text-secondary">{{ t('pages.scripts.marketplace.noScriptsFound') }}</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="!scriptContentOfMarketplaceVisible"
|
||||||
|
class="grid grid-cols-[repeat(auto-fill,minmax(280px,1fr))] gap-4"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="script in filteredMarketplaceScripts"
|
||||||
|
:key="script.downloadUrl"
|
||||||
|
class="flex flex-col gap-3 rounded-xl border border-border-secondary p-4 transition-all hover:border-accent hover:shadow-md"
|
||||||
|
>
|
||||||
|
<div class="flex items-start justify-between">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<FileCode :size="20" class="text-accent" />
|
||||||
|
<span class="font-semibold text-main">{{ script.name }}</span>
|
||||||
|
</div>
|
||||||
|
<span class="rounded bg-bg-tertiary px-2 py-0.5 text-xs text-secondary">v{{ script.version }}</span>
|
||||||
|
</div>
|
||||||
|
<p class="line-clamp-2 min-h-[40px] text-sm text-secondary">{{ script.description || '-' }}</p>
|
||||||
|
<div class="flex items-center gap-2 text-xs text-tertiary">
|
||||||
|
<UserIcon :size="12" />
|
||||||
|
<span>{{ script.author }}</span>
|
||||||
|
<span class="mx-1">•</span>
|
||||||
|
<span class="rounded bg-gray-400 px-1.5 py-0.5 text-white">
|
||||||
|
{{ supportedScriptCategories.find(cat => cat.type === script.category)?.name || script.category }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="mt-auto flex gap-2 pt-2">
|
||||||
|
<button
|
||||||
|
class="flex w-full items-center justify-center gap-2 rounded-lg border border-border bg-accent/20 px-4 py-2 text-sm font-semibold text-accent"
|
||||||
|
@click="openScriptDetails(script.content || '')"
|
||||||
|
>
|
||||||
|
<Edit2Icon :size="16" />
|
||||||
|
{{ t('pages.scripts.marketplace.showScriptCode') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="!isScriptDownloaded(script)"
|
||||||
|
class="flex w-full items-center justify-center gap-2 rounded-lg bg-success/90 px-4 py-2 text-sm font-semibold text-white transition-all hover:bg-success disabled:opacity-50"
|
||||||
|
:disabled="downloadingScripts.has(script.downloadUrl)"
|
||||||
|
@click="downloadMarketplaceScript(script)"
|
||||||
|
>
|
||||||
|
<template v-if="downloadingScripts.has(script.downloadUrl)">
|
||||||
|
<div class="h-4 w-4 animate-spin rounded-full border-2 border-white border-t-transparent" />
|
||||||
|
{{ t('pages.scripts.marketplace.downloading') }}
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<DownloadIcon :size="16" />
|
||||||
|
{{ t('pages.scripts.marketplace.download') }}
|
||||||
|
</template>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="isScriptDownloaded(script)"
|
||||||
|
class="flex w-full items-center justify-center gap-2 rounded-lg border border-success bg-success/20 px-4 py-2 text-sm font-semibold text-success"
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
<CheckIcon :size="16" />
|
||||||
|
{{ t('pages.scripts.marketplace.downloaded') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="flex h-full flex-col gap-4">
|
||||||
|
<Editor v-model="marketplaceScriptContent" language="javascript" :read-only="true" />
|
||||||
|
<CustomButton
|
||||||
|
class="mt-4"
|
||||||
|
type="primary"
|
||||||
|
:text="t('common.cancel')"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
scriptContentOfMarketplaceVisible = false
|
||||||
|
marketplaceScriptContent = ''
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CustomModal>
|
||||||
|
|
||||||
|
<CustomModal
|
||||||
|
v-if="shareDialogVisible"
|
||||||
|
v-model:visible="shareDialogVisible"
|
||||||
|
:title="t('pages.scripts.marketplace.shareScript')"
|
||||||
|
height="auto"
|
||||||
|
width="500px"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col gap-4 p-6">
|
||||||
|
<div v-if="!githubAuth.isAuthenticated" class="flex flex-col items-center gap-4 py-8">
|
||||||
|
<svg width="48" height="48" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<title>GitHub</title>
|
||||||
|
<path
|
||||||
|
d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<p class="text-center text-sm text-secondary">{{ t('pages.scripts.marketplace.loginRequired') }}</p>
|
||||||
|
<CustomButton
|
||||||
|
type="primary"
|
||||||
|
:icon="null"
|
||||||
|
:text="t('pages.scripts.marketplace.loginWithGitHub')"
|
||||||
|
@click="handleGitHubLogin"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<svg width="18" height="18" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<title>GitHub</title>
|
||||||
|
<path
|
||||||
|
d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
</CustomButton>
|
||||||
|
</div>
|
||||||
|
<template v-else>
|
||||||
|
<SettingCard class="w-full">
|
||||||
|
<CustomInput
|
||||||
|
v-model="shareMetadata.name"
|
||||||
|
:title="t('pages.scripts.marketplace.scriptName')"
|
||||||
|
:placeholder="t('pages.scripts.marketplace.scriptName')"
|
||||||
|
/>
|
||||||
|
</SettingCard>
|
||||||
|
<SettingCard class="w-full">
|
||||||
|
<CustomInput
|
||||||
|
v-model="shareMetadata.author"
|
||||||
|
:title="t('pages.scripts.marketplace.scriptAuthor')"
|
||||||
|
:placeholder="t('pages.scripts.marketplace.scriptAuthor')"
|
||||||
|
/>
|
||||||
|
</SettingCard>
|
||||||
|
<SettingCard class="w-full">
|
||||||
|
<CustomInput
|
||||||
|
v-model="shareMetadata.description"
|
||||||
|
:title="t('pages.scripts.marketplace.scriptDescription')"
|
||||||
|
:placeholder="t('pages.scripts.marketplace.scriptDescription')"
|
||||||
|
/>
|
||||||
|
</SettingCard>
|
||||||
|
<SettingCard class="w-full">
|
||||||
|
<CustomInput
|
||||||
|
v-model="shareMetadata.version"
|
||||||
|
:title="t('pages.scripts.marketplace.scriptVersion')"
|
||||||
|
:placeholder="t('pages.scripts.marketplace.scriptVersion')"
|
||||||
|
/>
|
||||||
|
</SettingCard>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<CustomButton type="secondary" :text="t('common.cancel')" @click="shareDialogVisible = false" />
|
||||||
|
<CustomButton
|
||||||
|
v-if="githubAuth.isAuthenticated"
|
||||||
|
type="primary"
|
||||||
|
:text="sharingScript ? t('pages.scripts.marketplace.sharing') : t('pages.scripts.marketplace.share')"
|
||||||
|
:disabled="sharingScript"
|
||||||
|
@click="handleShareScript"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</CustomModal>
|
||||||
|
|
||||||
|
<CustomModal
|
||||||
|
v-if="deviceFlowDialogVisible"
|
||||||
|
v-model:visible="deviceFlowDialogVisible"
|
||||||
|
:title="t('pages.scripts.marketplace.loginWithGitHub')"
|
||||||
|
width="500px"
|
||||||
|
height="auto"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col items-center gap-6 p-6">
|
||||||
|
<svg width="48" height="48" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<title>GitHub</title>
|
||||||
|
<path
|
||||||
|
d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<p class="text-center text-secondary">
|
||||||
|
{{ t('pages.scripts.marketplace.deviceFlowInstructions') }}
|
||||||
|
</p>
|
||||||
|
<div class="flex flex-col items-center gap-2">
|
||||||
|
<span class="text-sm text-secondary">{{ t('pages.scripts.marketplace.yourCode') }}</span>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<code class="rounded-lg bg-bg-tertiary px-6 py-3 text-2xl font-bold tracking-widest text-accent">
|
||||||
|
{{ deviceFlowState.userCode }}
|
||||||
|
</code>
|
||||||
|
<button
|
||||||
|
class="rounded-lg bg-bg-secondary p-2 transition-colors hover:bg-bg-tertiary"
|
||||||
|
:title="t('pages.scripts.marketplace.copyCode')"
|
||||||
|
@click="copyUserCode"
|
||||||
|
>
|
||||||
|
<CheckIcon v-if="false" :size="20" class="text-success" />
|
||||||
|
<svg
|
||||||
|
v-else
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class="text-secondary"
|
||||||
|
>
|
||||||
|
<rect width="14" height="14" x="8" y="8" rx="2" ry="2" />
|
||||||
|
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="h-5 w-5 animate-spin rounded-full border-2 border-border border-t-accent" />
|
||||||
|
<span class="text-sm text-secondary">{{ t('pages.scripts.marketplace.waitingForAuth') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<CustomButton type="secondary" :text="t('common.cancel')" @click="cancelDeviceFlow" />
|
||||||
|
</template>
|
||||||
|
</CustomModal>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -214,14 +525,25 @@
|
|||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import {
|
import {
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
|
CheckIcon,
|
||||||
Clock,
|
Clock,
|
||||||
|
DownloadIcon,
|
||||||
Edit2Icon,
|
Edit2Icon,
|
||||||
|
ExternalLinkIcon,
|
||||||
FileCode,
|
FileCode,
|
||||||
FolderOpen,
|
FolderOpen,
|
||||||
|
LogOutIcon,
|
||||||
|
PackageIcon,
|
||||||
Pencil,
|
Pencil,
|
||||||
Play,
|
Play,
|
||||||
Plus,
|
Plus,
|
||||||
|
RefreshCwIcon,
|
||||||
|
SearchIcon,
|
||||||
|
Share2Icon,
|
||||||
|
StoreIcon,
|
||||||
Trash2,
|
Trash2,
|
||||||
|
UserIcon,
|
||||||
|
XCircleIcon,
|
||||||
XIcon,
|
XIcon,
|
||||||
} from 'lucide-vue-next'
|
} from 'lucide-vue-next'
|
||||||
import { computed, onBeforeMount, onBeforeUnmount, ref, watch } from 'vue'
|
import { computed, onBeforeMount, onBeforeUnmount, ref, watch } from 'vue'
|
||||||
@@ -255,6 +577,81 @@ const newScriptNameVisible = ref(false)
|
|||||||
const newScriptName = ref('')
|
const newScriptName = ref('')
|
||||||
const newScriptCategory = ref('manualTrigger')
|
const newScriptCategory = ref('manualTrigger')
|
||||||
|
|
||||||
|
// Marketplace related state
|
||||||
|
interface IScriptMeta {
|
||||||
|
name: string
|
||||||
|
author: string
|
||||||
|
description: string
|
||||||
|
version: string
|
||||||
|
fileName: string
|
||||||
|
category: string
|
||||||
|
content: string | null
|
||||||
|
downloadUrl: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IGitHubAuth {
|
||||||
|
isAuthenticated: boolean
|
||||||
|
username: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IDeviceFlowState {
|
||||||
|
isActive: boolean
|
||||||
|
userCode: string | null
|
||||||
|
verificationUri: string | null
|
||||||
|
expiresAt: number | null
|
||||||
|
pollingInterval: NodeJS.Timeout | null
|
||||||
|
}
|
||||||
|
|
||||||
|
const marketplaceVisible = ref(false)
|
||||||
|
const marketplaceLoading = ref(false)
|
||||||
|
const marketplaceError = ref(false)
|
||||||
|
const marketplaceScripts = ref<IScriptMeta[]>([])
|
||||||
|
const marketplaceSearch = ref('')
|
||||||
|
const marketplaceScriptContent = ref('')
|
||||||
|
const marketplaceCategoryFilter = ref<string[]>([])
|
||||||
|
const downloadingScripts = ref<Set<string>>(new Set())
|
||||||
|
const githubAuth = ref<IGitHubAuth>({ isAuthenticated: false, username: null })
|
||||||
|
|
||||||
|
// Device Flow state
|
||||||
|
const deviceFlowState = ref<IDeviceFlowState>({
|
||||||
|
isActive: false,
|
||||||
|
userCode: null,
|
||||||
|
verificationUri: null,
|
||||||
|
expiresAt: null,
|
||||||
|
pollingInterval: null,
|
||||||
|
})
|
||||||
|
const deviceFlowDialogVisible = ref(false)
|
||||||
|
const scriptContentOfMarketplaceVisible = ref(false)
|
||||||
|
const shareDialogVisible = ref(false)
|
||||||
|
const sharingScript = ref(false)
|
||||||
|
const scriptToShare = ref<IStringKeyMap | null>(null)
|
||||||
|
const shareMetadata = ref({
|
||||||
|
name: '',
|
||||||
|
author: '',
|
||||||
|
description: '',
|
||||||
|
version: '1.0.0',
|
||||||
|
})
|
||||||
|
|
||||||
|
const filteredMarketplaceScripts = computed(() => {
|
||||||
|
let scripts = marketplaceScripts.value
|
||||||
|
|
||||||
|
if (marketplaceSearch.value) {
|
||||||
|
const search = marketplaceSearch.value.toLowerCase()
|
||||||
|
scripts = scripts.filter(
|
||||||
|
s =>
|
||||||
|
s.name.toLowerCase().includes(search) ||
|
||||||
|
s.description.toLowerCase().includes(search) ||
|
||||||
|
s.author.toLowerCase().includes(search),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (marketplaceCategoryFilter.value.length > 0) {
|
||||||
|
scripts = scripts.filter(s => marketplaceCategoryFilter.value.includes(s.category))
|
||||||
|
}
|
||||||
|
|
||||||
|
return scripts
|
||||||
|
})
|
||||||
|
|
||||||
const supportedScriptCategories = [
|
const supportedScriptCategories = [
|
||||||
{ type: 'onSoftwareOpen', name: t('pages.scripts.scriptsTypes.onSoftwareOpen') },
|
{ type: 'onSoftwareOpen', name: t('pages.scripts.scriptsTypes.onSoftwareOpen') },
|
||||||
{ type: 'onSoftwareClose', name: t('pages.scripts.scriptsTypes.onSoftwareClose') },
|
{ type: 'onSoftwareClose', name: t('pages.scripts.scriptsTypes.onSoftwareClose') },
|
||||||
@@ -437,11 +834,240 @@ async function toggleScript(scriptPath: string[]) {
|
|||||||
await getScriptsMap()
|
await getScriptsMap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openScriptDetails(content: string) {
|
||||||
|
marketplaceScriptContent.value = content
|
||||||
|
scriptContentOfMarketplaceVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openMarketplace() {
|
||||||
|
marketplaceVisible.value = true
|
||||||
|
await checkGitHubAuth()
|
||||||
|
await fetchMarketplaceScripts()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchMarketplaceScripts() {
|
||||||
|
marketplaceLoading.value = true
|
||||||
|
marketplaceError.value = false
|
||||||
|
|
||||||
|
try {
|
||||||
|
const scripts = await window.electron.triggerRPC<IScriptMeta[]>(IRPCActionType.SCRIPT_MARKETPLACE_FETCH_LIST)
|
||||||
|
marketplaceScripts.value = scripts || []
|
||||||
|
console.log('Fetched marketplace scripts:', marketplaceScripts.value)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch marketplace scripts:', error)
|
||||||
|
marketplaceError.value = true
|
||||||
|
} finally {
|
||||||
|
marketplaceLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isScriptDownloaded(script: IScriptMeta): boolean {
|
||||||
|
const categoryPath = script.category.replace(/\./g, '/')
|
||||||
|
const scriptPath = `${categoryPath}/${script.fileName}`
|
||||||
|
const isDownloaded = existingPathsSet.value.has(scriptPath)
|
||||||
|
return isDownloaded
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadMarketplaceScript(script: IScriptMeta) {
|
||||||
|
downloadingScripts.value.add(script.downloadUrl)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await window.electron.triggerRPC<boolean>(
|
||||||
|
IRPCActionType.SCRIPT_MARKETPLACE_DOWNLOAD,
|
||||||
|
getRawData(script),
|
||||||
|
)
|
||||||
|
if (result) {
|
||||||
|
message.success(t('pages.scripts.marketplace.downloadSuccess'))
|
||||||
|
await getScriptsMap()
|
||||||
|
} else {
|
||||||
|
message.error(t('pages.scripts.marketplace.downloadFailed'))
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to download script:', error)
|
||||||
|
message.error(t('pages.scripts.marketplace.downloadFailed'))
|
||||||
|
} finally {
|
||||||
|
downloadingScripts.value.delete(script.downloadUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkGitHubAuth() {
|
||||||
|
try {
|
||||||
|
const auth = await window.electron.triggerRPC<IGitHubAuth>(IRPCActionType.SCRIPT_MARKETPLACE_CHECK_GITHUB_AUTH)
|
||||||
|
if (auth) {
|
||||||
|
githubAuth.value = auth
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to check GitHub auth:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleGitHubLogin() {
|
||||||
|
try {
|
||||||
|
const result = await window.electron.triggerRPC<{
|
||||||
|
userCode: string
|
||||||
|
verificationUri: string
|
||||||
|
expiresIn: number
|
||||||
|
} | null>(IRPCActionType.SCRIPT_MARKETPLACE_GITHUB_LOGIN)
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
deviceFlowState.value = {
|
||||||
|
isActive: true,
|
||||||
|
userCode: result.userCode,
|
||||||
|
verificationUri: result.verificationUri,
|
||||||
|
expiresAt: Date.now() + result.expiresIn * 1000,
|
||||||
|
pollingInterval: null,
|
||||||
|
}
|
||||||
|
deviceFlowDialogVisible.value = true
|
||||||
|
|
||||||
|
startDeviceFlowPolling()
|
||||||
|
} else {
|
||||||
|
message.error(t('pages.scripts.marketplace.loginFailed'))
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to initiate GitHub login:', error)
|
||||||
|
message.error(t('pages.scripts.marketplace.loginFailed'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startDeviceFlowPolling() {
|
||||||
|
if (deviceFlowState.value.pollingInterval) {
|
||||||
|
clearInterval(deviceFlowState.value.pollingInterval)
|
||||||
|
}
|
||||||
|
|
||||||
|
const poll = async () => {
|
||||||
|
if (deviceFlowState.value.expiresAt && Date.now() > deviceFlowState.value.expiresAt) {
|
||||||
|
stopDeviceFlowPolling()
|
||||||
|
message.error(t('pages.scripts.marketplace.deviceCodeExpired'))
|
||||||
|
deviceFlowDialogVisible.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await window.electron.triggerRPC<{
|
||||||
|
success: boolean
|
||||||
|
username?: string
|
||||||
|
error?: string
|
||||||
|
nextInterval?: number
|
||||||
|
}>(IRPCActionType.SCRIPT_MARKETPLACE_GITHUB_POLL)
|
||||||
|
|
||||||
|
if (result?.success) {
|
||||||
|
stopDeviceFlowPolling()
|
||||||
|
githubAuth.value = { isAuthenticated: true, username: result.username || null }
|
||||||
|
deviceFlowDialogVisible.value = false
|
||||||
|
message.success(t('pages.scripts.marketplace.loginSuccess'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result?.error === 'authorization_pending' || result?.error === 'slow_down') {
|
||||||
|
const delay = (result.nextInterval || 5) * 1000
|
||||||
|
deviceFlowState.value.pollingInterval = setTimeout(poll, delay)
|
||||||
|
} else {
|
||||||
|
stopDeviceFlowPolling()
|
||||||
|
deviceFlowDialogVisible.value = false
|
||||||
|
message.error(`${t('pages.scripts.marketplace.loginFailed')}: ${result?.error}`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to poll device flow:', error)
|
||||||
|
deviceFlowState.value.pollingInterval = setTimeout(poll, 5000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
poll()
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopDeviceFlowPolling() {
|
||||||
|
if (deviceFlowState.value.pollingInterval) {
|
||||||
|
clearInterval(deviceFlowState.value.pollingInterval)
|
||||||
|
deviceFlowState.value.pollingInterval = null
|
||||||
|
}
|
||||||
|
deviceFlowState.value.isActive = false
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cancelDeviceFlow() {
|
||||||
|
stopDeviceFlowPolling()
|
||||||
|
await window.electron.triggerRPC(IRPCActionType.SCRIPT_MARKETPLACE_GITHUB_CANCEL)
|
||||||
|
deviceFlowDialogVisible.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyUserCode() {
|
||||||
|
if (deviceFlowState.value.userCode) {
|
||||||
|
window.electron.clipboard.writeText(deviceFlowState.value.userCode)
|
||||||
|
message.success(t('pages.scripts.marketplace.codeCopied'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleGitHubLogout() {
|
||||||
|
try {
|
||||||
|
await window.electron.triggerRPC(IRPCActionType.SCRIPT_MARKETPLACE_GITHUB_LOGOUT)
|
||||||
|
githubAuth.value = { isAuthenticated: false, username: null }
|
||||||
|
message.success(t('pages.scripts.marketplace.logoutSuccess'))
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to logout:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openMarketplaceRepo() {
|
||||||
|
window.electron.sendRPC(IRPCActionType.OPEN_URL, 'https://github.com/Kuingsmile/piclist-ScriptsHub')
|
||||||
|
}
|
||||||
|
|
||||||
|
function openShareDialog(script: IStringKeyMap) {
|
||||||
|
scriptToShare.value = script
|
||||||
|
shareMetadata.value = {
|
||||||
|
name: script.fileName.replace('.js', ''),
|
||||||
|
author: githubAuth.value.username || '',
|
||||||
|
description: '',
|
||||||
|
version: '1.0.0',
|
||||||
|
}
|
||||||
|
shareDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleShareScript() {
|
||||||
|
if (!scriptToShare.value) return
|
||||||
|
|
||||||
|
if (
|
||||||
|
!shareMetadata.value.name ||
|
||||||
|
!shareMetadata.value.author ||
|
||||||
|
!shareMetadata.value.description ||
|
||||||
|
!shareMetadata.value.version
|
||||||
|
) {
|
||||||
|
message.error(t('pages.scripts.marketplace.metadataRequired'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sharingScript.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await window.electron.triggerRPC<{ success: boolean; prUrl?: string; error?: string }>(
|
||||||
|
IRPCActionType.SCRIPT_MARKETPLACE_SHARE,
|
||||||
|
getRawData(scriptToShare.value.filePath),
|
||||||
|
getRawData(shareMetadata.value),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (result?.success) {
|
||||||
|
message.success(t('pages.scripts.marketplace.shareSuccess'))
|
||||||
|
shareDialogVisible.value = false
|
||||||
|
if (result.prUrl) {
|
||||||
|
window.electron.sendRPC(IRPCActionType.OPEN_URL, result.prUrl)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
message.error(`${t('pages.scripts.marketplace.shareFailed')}: ${result?.error || 'Unknown error'}`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to share script:', error)
|
||||||
|
message.error(t('pages.scripts.marketplace.shareFailed'))
|
||||||
|
} finally {
|
||||||
|
sharingScript.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onBeforeMount(async () => {
|
onBeforeMount(async () => {
|
||||||
getScriptsMap()
|
getScriptsMap()
|
||||||
|
await checkGitHubAuth()
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {})
|
onBeforeUnmount(() => {
|
||||||
|
stopDeviceFlowPolling()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
|||||||
@@ -95,6 +95,8 @@ export interface IConfigStruct {
|
|||||||
uploader: IUploaderConfig
|
uploader: IUploaderConfig
|
||||||
scripts: {
|
scripts: {
|
||||||
disabledList: string[]
|
disabledList: string[]
|
||||||
|
githubToken?: string
|
||||||
|
githubUsername?: string
|
||||||
}
|
}
|
||||||
buildIn: {
|
buildIn: {
|
||||||
compress: IBuildInCompressOptions
|
compress: IBuildInCompressOptions
|
||||||
@@ -198,6 +200,8 @@ export const configPaths = {
|
|||||||
uploader: 'uploader',
|
uploader: 'uploader',
|
||||||
scripts: {
|
scripts: {
|
||||||
disabledList: 'scripts.disabledList',
|
disabledList: 'scripts.disabledList',
|
||||||
|
githubToken: 'scripts.githubToken',
|
||||||
|
githubUsername: 'scripts.githubUsername',
|
||||||
},
|
},
|
||||||
buildIn: {
|
buildIn: {
|
||||||
_name: 'buildIn',
|
_name: 'buildIn',
|
||||||
|
|||||||
@@ -82,6 +82,16 @@ export const IRPCActionType = {
|
|||||||
WRITE_SCRIPT_FILE: 'WRITE_SCRIPT_FILE',
|
WRITE_SCRIPT_FILE: 'WRITE_SCRIPT_FILE',
|
||||||
DELETE_SCRIPTS_FILE: 'DELETE_SCRIPTS_FILE',
|
DELETE_SCRIPTS_FILE: 'DELETE_SCRIPTS_FILE',
|
||||||
|
|
||||||
|
// script marketplace rpc
|
||||||
|
SCRIPT_MARKETPLACE_FETCH_LIST: 'SCRIPT_MARKETPLACE_FETCH_LIST',
|
||||||
|
SCRIPT_MARKETPLACE_DOWNLOAD: 'SCRIPT_MARKETPLACE_DOWNLOAD',
|
||||||
|
SCRIPT_MARKETPLACE_SHARE: 'SCRIPT_MARKETPLACE_SHARE',
|
||||||
|
SCRIPT_MARKETPLACE_CHECK_GITHUB_AUTH: 'SCRIPT_MARKETPLACE_CHECK_GITHUB_AUTH',
|
||||||
|
SCRIPT_MARKETPLACE_GITHUB_LOGIN: 'SCRIPT_MARKETPLACE_GITHUB_LOGIN',
|
||||||
|
SCRIPT_MARKETPLACE_GITHUB_POLL: 'SCRIPT_MARKETPLACE_GITHUB_POLL',
|
||||||
|
SCRIPT_MARKETPLACE_GITHUB_CANCEL: 'SCRIPT_MARKETPLACE_GITHUB_CANCEL',
|
||||||
|
SCRIPT_MARKETPLACE_GITHUB_LOGOUT: 'SCRIPT_MARKETPLACE_GITHUB_LOGOUT',
|
||||||
|
|
||||||
// shortkey setting rpc
|
// shortkey setting rpc
|
||||||
SHORTKEY_UPDATE: 'SHORTKEY_UPDATE',
|
SHORTKEY_UPDATE: 'SHORTKEY_UPDATE',
|
||||||
SHORTKEY_BIND_OR_UNBIND: 'SHORTKEY_BIND_OR_UNBIND',
|
SHORTKEY_BIND_OR_UNBIND: 'SHORTKEY_BIND_OR_UNBIND',
|
||||||
|
|||||||
Reference in New Issue
Block a user