Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/app/src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions packages/app/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ pub fn run() {
#[cfg(target_os = "macos")]
let builder = builder.plugin(tauri_nspanel::init());

let builder = skin_import::register_skin_protocol(builder);

builder
.setup(|app| {
tray::setup_tray(app)?;
Expand Down
75 changes: 74 additions & 1 deletion packages/app/src-tauri/src/skin_import.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,80 @@ use serde::Serialize;
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use tauri::Manager;
use tauri::http;
use tauri::{Manager, Runtime};

fn mime_from_ext(path: &str) -> &'static str {
match path.rsplit('.').next().unwrap_or("") {
"json" => "application/json",
"png" => "image/png",
"gif" => "image/gif",
"jpg" | "jpeg" => "image/jpeg",
"webp" => "image/webp",
"svg" => "image/svg+xml",
_ => "application/octet-stream",
}
}

fn file_response(file_path: &Path, uri_path: &str) -> http::Response<Vec<u8>> {
match fs::read(file_path) {
Ok(data) => http::Response::builder()
.header(http::header::CONTENT_TYPE, mime_from_ext(uri_path))
.header(http::header::ACCESS_CONTROL_ALLOW_ORIGIN, "*")
.body(data)
.unwrap(),
Err(_) => http::Response::builder()
.status(http::StatusCode::INTERNAL_SERVER_ERROR)
.body(Vec::new())
.unwrap(),
}
}

pub fn register_skin_protocol<R: Runtime>(builder: tauri::Builder<R>) -> tauri::Builder<R> {
builder.register_uri_scheme_protocol("skin", |ctx, request| {
let path = request.uri().path().trim_start_matches('/');

if path.is_empty() || path.contains("..") {
return http::Response::builder()
.status(http::StatusCode::BAD_REQUEST)
.body(Vec::new())
.unwrap();
}

if cfg!(debug_assertions) {
let dev_path = Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap_or(Path::new("."))
.join("public")
.join("skins")
.join(path);
if dev_path.is_file() {
return file_response(&dev_path, path);
}
} else {
if let Ok(data_dir) = ctx.app_handle().path().app_data_dir() {
let user_path = data_dir.join("skins").join(path);
if user_path.is_file() {
return file_response(&user_path, path);
}
}

let asset_path = format!("skins/{}", path);
if let Some(asset) = ctx.app_handle().asset_resolver().get(asset_path) {
return http::Response::builder()
.header(http::header::CONTENT_TYPE, &asset.mime_type)
.header(http::header::ACCESS_CONTROL_ALLOW_ORIGIN, "*")
.body(asset.bytes)
.unwrap();
}
}

http::Response::builder()
.status(http::StatusCode::NOT_FOUND)
.body(Vec::new())
.unwrap()
})
}

include!(concat!(env!("OUT_DIR"), "/builtin_skins.rs"));

Expand Down
2 changes: 1 addition & 1 deletion packages/app/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
}
],
"security": {
"csp": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' asset: https://asset.localhost data: blob:; connect-src 'self' ipc: http://ipc.localhost https://github.com https://api.github.com"
"csp": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' asset: https://asset.localhost skin: https://skin.localhost data: blob:; connect-src 'self' ipc: http://ipc.localhost skin: https://skin.localhost https://github.com https://api.github.com"
},
"trayIcon": null
},
Expand Down
3 changes: 2 additions & 1 deletion packages/app/src/panels/AboutView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { openUrl } from '@tauri-apps/plugin-opener'
import { useI18n } from '@/composables/useI18n'
import { useAutoUpdater } from '@/composables/useAutoUpdater'
import { SKIN_BASE } from '@/stores/skin'

const { t } = useI18n()
const appVersion = __APP_VERSION__
Expand Down Expand Up @@ -30,7 +31,7 @@ async function open(url: string) {
</header>

<div class="about-brand">
<img class="about-logo" src="/skins/vita/pet.png" alt="AIbubu" />
<img class="about-logo" :src="`${SKIN_BASE}/vita/pet.png`" alt="AIbubu" />
<div class="brand-info">
<p class="brand-name">{{ t('brand') }}</p>
<p class="brand-desc">{{ t('aboutDesc') }}</p>
Expand Down
3 changes: 2 additions & 1 deletion packages/app/src/panels/PeerAvatar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { PeerInfo } from '@/types'
import { MOVEMENT_COLORS } from '@/utils/activity'
import { useI18n } from '@/composables/useI18n'
import { MOVEMENT_STATE_KEYS } from '@/utils/movement'
import { SKIN_BASE } from '@/stores/skin'

const { t } = useI18n()

Expand All @@ -20,7 +21,7 @@ const rankDisplay = computed(() => (props.rank ? String(props.rank) : ''))
const avatarFailed = ref(false)
const avatarSrc = computed(() => {
if (props.peer.petSkin && !avatarFailed.value) {
return `/skins/${props.peer.petSkin}/pet.png`
return `${SKIN_BASE}/${props.peer.petSkin}/pet.png`
}
return ''
})
Expand Down
6 changes: 3 additions & 3 deletions packages/app/src/panels/SkinMarket.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { openUrl } from '@tauri-apps/plugin-opener'
import { useSkinStore } from '@/stores/skin'
import { useSkinStore, SKIN_BASE } from '@/stores/skin'
import { useSettingsStore } from '@/stores/settings'
import { getScoreColor } from '@/utils/activity'
import { resolveAnimation } from '@/utils/skin'
Expand Down Expand Up @@ -55,7 +55,7 @@ function buildPreviewCells(entry: CatalogEntry): PreviewCell[] {
state: s.state,
score: s.score,
anim: result ? result.config : null,
src: result ? `/skins/${entry.id}/${result.config.file}` : '',
src: result ? `${SKIN_BASE}/${entry.id}/${result.config.file}` : '',
}
})
}
Expand All @@ -69,7 +69,7 @@ const hoveredPreviewCells = computed(() =>
)

function petAvatarSrc(skinId: string): string {
return `/skins/${skinId}/pet.png`
return `${SKIN_BASE}/${skinId}/pet.png`
}

function onEnter(id: string) {
Expand Down
3 changes: 2 additions & 1 deletion packages/app/src/panels/SocialPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useSocialStore } from '@/stores/social'
import { useSettingsStore } from '@/stores/settings'
import { useI18n } from '@/composables/useI18n'
import { useAutoUpdater } from '@/composables/useAutoUpdater'
import { SKIN_BASE } from '@/stores/skin'
import Leaderboard from './Leaderboard.vue'
import TodayView from './TodayView.vue'
import SkinMarket from './SkinMarket.vue'
Expand Down Expand Up @@ -106,7 +107,7 @@ onUnmounted(() => {
<div class="social-window" :class="{ 'theme-light': isLight }">
<nav class="sidebar">
<div class="sidebar-brand">
<img class="brand-logo" src="/skins/vita/pet.png" :alt="t('brand')" />
<img class="brand-logo" :src="`${SKIN_BASE}/vita/pet.png`" :alt="t('brand')" />
<span class="brand-text">{{ t('brand') }}</span>
<button
class="lang-toggle"
Expand Down
4 changes: 2 additions & 2 deletions packages/app/src/pet/OverflowBadge.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { ref, computed, nextTick, onUnmounted } from 'vue'
import type { PeerInfo } from '@/types'
import { useI18n } from '@/composables/useI18n'
import { useSkinStore } from '@/stores/skin'
import { useSkinStore, SKIN_BASE } from '@/stores/skin'
import MoodIcon from './MoodIcon.vue'

const { t, isZh } = useI18n()
Expand All @@ -29,7 +29,7 @@ const peerRows = computed(() =>
const name = p.nickname || t('lbSelf')
const steps = p.dailySteps.toLocaleString()
const stepsLabel = isZh.value ? `${steps}步` : `${steps} steps`
const avatarUrl = skinStore.isBuiltin(p.petSkin) ? `/skins/${p.petSkin}/pet.png` : null
const avatarUrl = skinStore.isBuiltin(p.petSkin) ? `${SKIN_BASE}/${p.petSkin}/pet.png` : null
const initial = (p.nickname || '?').charAt(0)
const moodState = p.moodState ?? 'normal'
return { name, stepsLabel, avatarUrl, initial, moodState }
Expand Down
4 changes: 2 additions & 2 deletions packages/app/src/pet/PeerMiniature.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { computed, ref, watch, onMounted } from 'vue'
import type { PeerInfo, SkinManifest } from '@/types'
import { useI18n } from '@/composables/useI18n'
import { useSkinStore } from '@/stores/skin'
import { useSkinStore, SKIN_BASE } from '@/stores/skin'
import { resolveAnimation } from '@/utils/skin'
import { MOVEMENT_STATE_KEYS } from '@/utils/movement'
import SpriteRenderer from './renderers/SpriteRenderer.vue'
Expand All @@ -22,7 +22,7 @@ const manifest = ref<SkinManifest | null>(null)
const loadFailed = ref(false)
const hovered = ref(false)

const skinBasePath = computed(() => `/skins/${props.peer.petSkin}/`)
const skinBasePath = computed(() => `${SKIN_BASE}/${props.peer.petSkin}/`)

const currentAnimation = computed(() => {
if (!manifest.value) return null
Expand Down
6 changes: 3 additions & 3 deletions packages/app/src/stores/skin.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { setActivePinia, createPinia } from 'pinia'
import { useSkinStore } from './skin'
import { useSkinStore, SKIN_BASE } from './skin'
import type { SkinManifest } from '@/types'

vi.mock('@tauri-apps/api/core', () => ({
Expand Down Expand Up @@ -51,7 +51,7 @@ describe('useSkinStore', () => {

it('skinBasePath reflects current skin id', () => {
const store = useSkinStore()
expect(store.skinBasePath).toBe('/skins/vita/')
expect(store.skinBasePath).toBe(`${SKIN_BASE}/vita/`)
})

describe('loadCatalog', () => {
Expand Down Expand Up @@ -152,7 +152,7 @@ describe('useSkinStore', () => {

const result = store.getAnimationForState('idle')
expect(result).not.toBeNull()
expect(result!.src).toBe('/skins/vita/idle.png')
expect(result!.src).toBe(`${SKIN_BASE}/vita/idle.png`)
expect(result!.state).toBe('idle')
})

Expand Down
10 changes: 6 additions & 4 deletions packages/app/src/stores/skin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { useI18n } from '@/composables/useI18n'
import type { SkinManifest, MovementState, SkinAnimationConfig } from '@/types'
import { resolveAnimation } from '@/utils/skin'

export const SKIN_BASE = 'skin://localhost'

interface SkinEntry {
id: string
builtin: boolean
Expand Down Expand Up @@ -36,7 +38,7 @@ export const useSkinStore = defineStore('skin', () => {
size: { width: 48, height: 48 },
animations: {},
})
const skinBasePath = computed(() => `/skins/${currentManifest.value.id}/`)
const skinBasePath = computed(() => `${SKIN_BASE}/${currentManifest.value.id}/`)

function getAnimationForState(state: MovementState): {
state: MovementState
Expand All @@ -63,7 +65,7 @@ export const useSkinStore = defineStore('skin', () => {
abortController = new AbortController()

try {
const resp = await fetch(`/skins/${skinId}/skin.json`, {
const resp = await fetch(`${SKIN_BASE}/${skinId}/skin.json`, {
signal: abortController.signal,
})
if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
Expand All @@ -84,7 +86,7 @@ export const useSkinStore = defineStore('skin', () => {

const results = await Promise.allSettled(
skinEntries.map(async ({ id }) => {
const r = await fetch(`/skins/${id}/skin.json`)
const r = await fetch(`${SKIN_BASE}/${id}/skin.json`)
if (!r.ok) throw new Error(`HTTP ${r.status}`)
const manifest: SkinManifest = await r.json()
manifest.id = id
Expand Down Expand Up @@ -122,7 +124,7 @@ export const useSkinStore = defineStore('skin', () => {
if (cached) return cached

try {
const resp = await fetch(`/skins/${skinId}/skin.json`)
const resp = await fetch(`${SKIN_BASE}/${skinId}/skin.json`)
if (!resp.ok) return null
const manifest: SkinManifest = await resp.json()
manifest.id = skinId
Expand Down
Loading