Skip to content

Commit

Permalink
feat: Support drag drop to add third party yggdrasil service
Browse files Browse the repository at this point in the history
  • Loading branch information
ci010 committed Jan 29, 2023
1 parent 272c893 commit 53c1b2f
Show file tree
Hide file tree
Showing 19 changed files with 491 additions and 230 deletions.
3 changes: 2 additions & 1 deletion xmcl-keystone-ui/locales/en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -943,6 +943,7 @@ save:
manage: Manage Saves
name: Save | Saves
screenshots:
empty: You don't have screenshot
goto: Open Folder
name: Screenshots
server:
Expand Down Expand Up @@ -1202,7 +1203,7 @@ userService:
requireName: The name cannot be empty
texture: Upload Texture API
textureHint: Upload Texture by User ID and Access Token
title: User Services
title: Third-party User Services
typeOfService: Choose the Service Type
validateHint: Used to check if user's token still valid
userServices:
Expand Down
2 changes: 1 addition & 1 deletion xmcl-keystone-ui/locales/zh-CN.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1183,7 +1183,7 @@ userService:
service: null
texture: 皮肤上传 API
textureHint: 通过玩家 ID 和凭证来上传皮肤
title: 配置验证或玩家信息服务器
title: 第三方玩家信息服务
typeOfService: 选择配置类型
validateHint: 用来检验玩家凭证有效性
userServices:
Expand Down
2 changes: 1 addition & 1 deletion xmcl-keystone-ui/locales/zh-TW.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -639,7 +639,7 @@ userService:
requireName: 服務器名不能為空
texture: 皮膚上傳 API
textureHint: 通過玩家 ID 和憑證來上傳皮膚
title: 配置驗證或玩家信息服務器
title: 第三方玩家信息服務
typeOfService: 選擇配置類型
validateHint: 用來檢驗玩家憑證有效性
userServices:
Expand Down
274 changes: 198 additions & 76 deletions xmcl-keystone-ui/src/composables/dropService.ts
Original file line number Diff line number Diff line change
@@ -1,91 +1,215 @@
import { InjectionKey, Ref } from 'vue'
import { BaseServiceKey, ImportServiceKey, isPersistedResource, Resource, ResourceDomain, ResourceServiceKey, ResourceType } from '@xmcl/runtime-api'
import { getExpectedSize } from '@/util/size'
import { BaseServiceKey, ImportServiceKey, isPersistedResource, Resource, ResourceDomain, ResourceServiceKey, UserServiceKey } from '@xmcl/runtime-api'
import { InjectionKey } from 'vue'
import { basename } from '../util/basename'
import { useService } from './service'

export interface FilePreview {
export interface PreviewItem {
id: string
enabled: boolean

// Used for ui display
status: 'loading' | 'idle' | 'failed' | 'saved'
name: string
path: string
url?: string[]
size: number
result: Resource | undefined
}
icon: string
title: string
description: string
type: string

export interface DropService {
loading: Ref<boolean>
active: Ref<boolean>
dragover: Ref<boolean>
previews: Ref<FilePreview[]>
suppressed: Ref<boolean>
remove(preview: FilePreview): void
cancel(): void
// Used for import
uris: string[]
resource: Resource | undefined
}

export const DropServiceInjectionKey: InjectionKey<DropService> = Symbol('DropService')
export const kDropService: InjectionKey<ReturnType<typeof useDropService>> = Symbol('DropService')

export function useDropService() {
const dragover = ref(false)
const active = ref(false)
const loading = ref(false)
const previews = ref([] as FilePreview[])
const previews = ref([] as PreviewItem[])
const { resolveResources } = useService(ResourceServiceKey)
const { handleUrl } = useService(BaseServiceKey)
const { previewUrl } = useService(ImportServiceKey)
const { addYggdrasilAccountSystem } = useService(UserServiceKey)
const { previewUrl, importFile } = useService(ImportServiceKey)
const suppressed = ref(false)
const { t } = useI18n()

const iconMap: Record<string, string> = {
forge: '$vuetify.icons.package',
fabric: '$vuetify.icons.fabric',
unclassified: 'question_mark',
resourcepack: '$vuetify.icons.zip',
shaderpack: '$vuetify.icons.zip',
'curseforge-modpack': '$vuetify.icons.curseforge',
modpack: '$vuetify.icons.package',
'mcbbs-modpack': '$vuetify.icons.package',
save: '$vuetify.icons.zip',
'modrinth-modpack': '$vuetify.icons.modrinth',
}

function getIcon(resource: Resource | undefined) {
return resource ? iconMap[resource.domain] ?? 'question_mark' : 'question_mark'
}

function getDescription(r: Resource | undefined, url: string) {
if (!r) {
return url
}
const size = getExpectedSize(r.size, 'B')
return `${size} ${url}`
}

function getType(resource: Resource | undefined) {
const types = [] as string[]
if (!resource) {
return t('universalDrop.unknownResource')
}
for (const key of Object.keys(resource.metadata)) {
switch (key) {
case 'forge':
types.push('Forge Mod')
break
case 'fabric':
types.push('Fabric Mod')
break
case 'resourcepack':
types.push(t('resourcepack.name', 0))
break
case 'mcbbs-modpack':
case 'modpack':
types.push(t('modpack.name', 0))
break
case 'save':
types.push(t('save.name', 0))
break
case 'curseforge-modpack':
types.push(t('modpack.name', 0))
break
case 'modrinth-modpack':
types.push(t('modrinth.projectType.modpack'))
break
case 'shaderpack':
types.push(t('shaderPack.name'))
break
}
}
return types.join(' | ')
}

async function onImport(previews: PreviewItem[]) {
const promises = [] as Promise<any>[]
for (const preview of previews) {
preview.status = 'loading'
if (preview.resource) {
const res = preview.resource
const promise = importFile({
resource: {
name: preview.title,
path: res.path,
uris: preview.uris,
},
modpackPolicy: {
import: true,
},
}).then(() => {
preview.type = getType(res)
preview.icon = getIcon(res)
preview.status = 'saved'
}, (e) => {
console.log(`Failed to import resource ${res.path}`)
console.log(e)
preview.status = 'failed'
})
promises.push(promise)
} else if (preview.type === 'Yggdrasil') {
addYggdrasilAccountSystem(preview.id).then(() => {
preview.status = 'saved'
}, (e) => {
console.log(e)
preview.status = 'failed'
})
}
}
Promise.all(promises).then(() => cancel())
}

async function previewAuthService(url: string) {
const existed = previews.value.find(v => v.id === url)
if (!existed) {
const object: PreviewItem = reactive({
enabled: true,
id: url,

type: 'Yggdrasil',
icon: 'link',
title: computed(() => t('userService.add')),
description: url,
status: 'idle' as const,

uris: [url],
resource: undefined,
})
previews.value.push(object)
}
}

async function previewGitUrl(url: string) {
const existed = previews.value.find(v => v.id === url)
const object: PreviewItem = existed ?? reactive({
enabled: false,
id: url,

type: getType(undefined),
icon: getIcon(undefined),
title: basename(new URL(url).pathname),
description: getDescription(undefined, url),
status: 'loading' as const,

uris: [url],
resource: undefined,
})

if (!existed || existed.status === 'failed') {
const promise = previewUrl({ url: url })
promise.then((result) => {
object.resource = result
if (result) {
object.title = result.name
object.type = getType(result)
object.icon = getIcon(result)

object.status = 'idle'
object.uris = result.uris
} else {
object.status = 'failed'
}
}, () => {
object.status = 'failed'
})
}

if (!existed) {
previews.value.push(object)
}
}

async function onDrop(event: DragEvent) {
const dataTransfer = event.dataTransfer!

console.log(dataTransfer.types[0])
console.log(dataTransfer.dropEffect)
console.log(dataTransfer.getData('text/html'))
console.log(dataTransfer.getData('text/plain'))

if (dataTransfer.items.length > 0) {
for (let i = 0; i < dataTransfer.items.length; ++i) {
const item = dataTransfer.items[i]
if (item.kind === 'string') {
const content = await new Promise<string>((resolve) => {
item.getAsString((content) => {
resolve(content)
})
})
const content = await new Promise<string>((resolve) => item.getAsString(resolve))
if (content.startsWith('authlib-injector:yggdrasil-server:')) {
handleUrl(content)
previewAuthService(content)
} else if (content.startsWith('https://github.com/') || content.startsWith('https://gitlab.com')) {
const existed = previews.value.find(v => v.url && v.url.some(v => v === content))
const object: FilePreview = existed ?? reactive({
url: [content],
size: -1,
path: '',
name: basename(new URL(content).pathname),
status: 'loading' as const,
enabled: false,
result: undefined,
})

if (!existed || existed.status === 'failed') {
const promise = previewUrl({ url: content })
promise.then((result) => {
object.result = result
if (result) {
object.size = result?.size
object.name = result.name
object.path = result.path
object.status = 'idle'
object.url = result.uris
} else {
object.status = 'failed'
}
}, () => {
object.status = 'failed'
})
}

if (!existed) {
previews.value.push(object)
}
previewGitUrl(content)
}
break
}
}
}
Expand All @@ -94,7 +218,7 @@ export function useDropService() {
if (dataTransfer.files.length > 0) {
for (let i = 0; i < dataTransfer.files.length; i++) {
const file = dataTransfer.files.item(i)!
if (previews.value.every(p => p.path !== file.path)) {
if (previews.value.every(p => p.id !== file.path)) {
files.push(file)
}
}
Expand All @@ -105,21 +229,26 @@ export function useDropService() {
const r = result[i]
const f = files[i]
previews.value.push({
...r,
name: f.name,
size: r.size,
result: r,
enabled: isPersistedResource(r),
id: f.path,

title: f.name,
description: getDescription(r, f.path),
icon: getIcon(r),
type: getType(r),
status: isPersistedResource(r) && r.domain !== ResourceDomain.Unclassified ? 'saved' : 'idle',

uris: [],
resource: r,
})
}
dragover.value = false
if (previews.value.length === 0) {
cancel()
}
}
function remove(file: FilePreview) {
previews.value = previews.value.filter((p) => p.path !== file.path)
function remove(file: PreviewItem) {
previews.value = previews.value.filter((p) => p.id !== file.id)
if (previews.value.length === 0) {
cancel()
}
Expand Down Expand Up @@ -162,20 +291,13 @@ export function useDropService() {
e.dataTransfer!.dropEffect = 'copy'
})

provide(DropServiceInjectionKey, {
loading,
active,
previews,
remove,
cancel,
dragover,
suppressed,
})
return {
dragover,
loading,
active,
previews,
suppressed,
onImport,
remove,
cancel,
}
Expand Down

0 comments on commit 53c1b2f

Please sign in to comment.