Compare commits

..
11 Commits
Author SHA1 Message Date
zhongyidianandClaude Opus 5 6491b0c918 fix: 优化进度调试与答题交互
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-21 10:29:54 +08:00
zhongyidian d445f81e60 chore: 更新素材资源和组件优化
- 更新陈云、鲁迅、聂耳等故居的展品图片素材
- 更新拼图细节图片
- 优化学习地图底图和详情图
- 更新音频素材
- 优化各故居 store 和页面组件
2026-08-12 11:35:09 +08:00
zhongyidianandClaude Opus 4.8 165374b986 fix: 修复从场景返回首页时误弹续学提示的问题
问题:用户从对话/体悟/内化场景点击「离开」返回故居首页时,会立即弹出「继续学习」提示,干扰正常流程。

修复:
- 新增 previousPath 模块级变量记录上一个路由
- 检测进入首页时上一个路由是否为本故居的 place/reflection/internalization/draw
- 如果是刚从场景离开的,不弹窗,让用户自主选择下一步

影响文件:
- src/hooks/useResidenceContinue.ts

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 14:32:40 +08:00
zhongyidianandClaude Opus 4.8 73f6df92ce chore: 更新素材和组件优化
- 更新展品图片(鲁迅/毛泽东/孙中山/黄炎培/聂耳/张乐平故居)
- 新增点击查看图标、报纸素材
- 优化视频组件、拼图组件、展品组件
- 更新陈云音频资源
- 新增输入验证文档和工具函数
- 组件布局和动画优化

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 21:50:40 +08:00
zhongyidianandClaude Opus 4.8 73d02eb62d feat: 实现故居续学弹窗功能
新增功能:
- 统一续学逻辑:8个故居首页接入自动续学提示弹窗
- 完成判定:基于拼图碎片统一判断对话/体悟/内化是否完成
- 断点续学:对话回lastVisitedPlace、体悟回reflectionRoute、内化回internalization
- 触发条件:会话内每故居仅提示一次 + 首页视频播完后 + store加载成功/空时
- 安全防护:组件卸载后不再setState、同步失败不弹窗避免用旧状态误导

新增组件:
- ContinueLearningModal:纯CSS哑光米纸+朱红古籍风弹窗
- useResidenceContinue:封装触发逻辑+跳转+去重的共享hook

涉及文件:
- src/config/progression.ts:getResidenceStageCompletion、resolveResidenceContinueTarget
- src/hooks/useResidenceContinue.ts:统一续学hook
- src/components/layout/alert/continue-learning-modal.tsx:弹窗组件
- src/pages/*/home.tsx:8个故居首页接入

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 21:44:27 +08:00
zhongyidianandClaude Opus 4.7 49ecabbf3a fix: 修复开发/测试包模拟进度被易班同步覆盖的问题
进度模拟面板写入 puzzleStore 后,App 启动时易班同步会将其覆盖;
现在在同步完成后按 sessionStorage 中保存的模式重放模拟进度,
确保测试包的"到达某故居/全流程完成"调试入口生效。

同时纳入近期未提交的页面、组件、资源与环境变量改动。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 10:19:36 +08:00
zhongyidianandClaude Opus 4.7 6d40e19346 feat: update residence learning flows
Commit non-media application and asset changes while leaving audio, video, and environment files out of version control.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 10:15:26 +08:00
zhongyidianandClaude Opus 4.7 68ba22a33f fix: update quiz flows and chenyun reflection progression
Add the requested Rong Desheng quiz gates and fix Chen Yun reflection progression so success prompts and trend chart state advance correctly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 09:54:21 +08:00
zhongyidian 284302341c feat: add progression logic for puzzle residences
- Implemented functions to manage puzzle progress including checking if residences are started, complete, and unlocked.
- Added utility functions to retrieve piece IDs based on residence and route.
- Introduced a new TypeScript file for progression management.
- Integrated puzzle data configuration and residence data for seamless progression tracking.
2026-05-01 21:19:25 +08:00
zhongyidianandClaude Opus 4.7 5a1c3cc368 Merge origin/master into main
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 10:01:23 +08:00
kevinzhu 4a2be3b4d0 feat:修改英文双引号 2026-04-30 09:17:31 +08:00
422 changed files with 16764 additions and 3951 deletions

No files matched your search

+15 -1
View File
@@ -45,7 +45,21 @@
"Bash(git -C /Users/zhongyidian/工作/育享/沪上又见故人来/shanghai-residences/.claude/worktrees/agent-a41031e2 show main:src/pages/zhangleping/home.tsx)", "Bash(git -C /Users/zhongyidian/工作/育享/沪上又见故人来/shanghai-residences/.claude/worktrees/agent-a41031e2 show main:src/pages/zhangleping/home.tsx)",
"Bash(git -C /Users/zhongyidian/工作/育享/沪上又见故人来/shanghai-residences/.claude/worktrees/agent-a41031e2 show main:src/store/storeNames.ts)", "Bash(git -C /Users/zhongyidian/工作/育享/沪上又见故人来/shanghai-residences/.claude/worktrees/agent-a41031e2 show main:src/store/storeNames.ts)",
"Bash(git -C /Users/zhongyidian/工作/育享/沪上又见故人来/shanghai-residences/.claude/worktrees/agent-a41031e2 grep -n \"getUserDefine\" main -- src/pages/zhangleping src/store/zhanglepingStore.ts src/App.tsx)", "Bash(git -C /Users/zhongyidian/工作/育享/沪上又见故人来/shanghai-residences/.claude/worktrees/agent-a41031e2 grep -n \"getUserDefine\" main -- src/pages/zhangleping src/store/zhanglepingStore.ts src/App.tsx)",
"Bash(git -C /Users/zhongyidian/工作/育享/沪上又见故人来/shanghai-residences/.claude/worktrees/agent-a41031e2 grep -n \"setDrawArtwork\\\\|clearDrawArtwork\\\\|drawArtworkDataUrl\" main -- src)" "Bash(git -C /Users/zhongyidian/工作/育享/沪上又见故人来/shanghai-residences/.claude/worktrees/agent-a41031e2 grep -n \"setDrawArtwork\\\\|clearDrawArtwork\\\\|drawArtworkDataUrl\" main -- src)",
"Bash(git grep *)",
"Bash(git commit -m ' *)",
"Bash(bun ls-files *)",
"Bash(bunx prettier *)",
"Bash(awk 'NR>=216 && NR<=380' src/config/puzzleData.ts)",
"Bash(awk 'NR>=380 && NR<=540' src/config/puzzleData.ts)",
"Bash(awk '/^worktree / {wt=$2} /^branch / {br=$2; if \\(wt ~ /\\\\.claude\\\\/worktrees\\\\/agent-/\\) print wt\"\\\\t\"br}')",
"Read(//tmp/**)",
"Bash(git check-ignore *)",
"Bash(git config *)",
"WebSearch",
"Bash(grep -v \"^--$\")",
"Bash(grep -E \"^\\\\S+.*>.*</GuideStepsItem>$\")",
"Bash(git -C \"/Users/zhongyidian/工作/育享/沪上又见故人来/shanghai-residences/.claude/worktrees/agent-ab7540fc239364906\" status --short)"
], ],
"deny": [ "deny": [
"Read(public/**)", "Read(public/**)",
+1
View File
@@ -0,0 +1 @@
VITE_DEBUG_ENTRY=false
+1
View File
@@ -0,0 +1 @@
VITE_DEBUG_ENTRY=false
+2
View File
@@ -0,0 +1,2 @@
VITE_DEBUG_ENTRY=true
VITE_DISABLE_FIRST_WATCH_VIDEO_GATING=true
+128
View File
@@ -0,0 +1,128 @@
# 信件输入校验功能说明
## 功能概述
为信件模板(`LetterTemplate.tsx`)的输入框添加了严格的内容校验,包括:
1. **禁止英文字母** - 自动过滤并提示
2. **禁止数字** - 自动过滤并提示
3. **禁止所有标点符号和特殊符号**(包括中文标点和英文标点)- 自动过滤并提示
4. **敏感词过滤** - 检测并替换为星号,提交时校验
## 实现细节
### 1. 核心函数(`src/lib/utils/sensitiveWords.ts`)
#### `filterInput(input: string): string`
实时过滤输入内容,在用户输入时自动去除:
- 英文字母(a-z, A-Z)
- 数字(0-9)
- 所有标点符号(包括中文标点:,。!?;:等)
- 所有特殊符号(@#$%^&*等)
- 敏感词(替换为星号)
#### `validateInput(input: string): { valid: boolean, message: string }`
提交时完整校验,返回详细的错误信息:
- 空内容检测
- 英文字母检测
- 数字检测
- 特殊符号检测
- 敏感词检测
#### 辅助函数
- `containsEnglish()` - 检测是否包含英文
- `containsNumber()` - 检测是否包含数字
- `containsSpecialChars()` - 检测是否包含标点符号或特殊符号(包括中文标点)
- `containsSensitiveWord()` - 检测是否包含敏感词
### 2. 敏感词库
已扩展的敏感词库包括:
- 基础不文明用语
- 暴力相关词汇
- 违法违规内容
- 歧视性用语
可根据实际需求在 `SENSITIVE_WORDS` 数组中扩展。
### 3. 用户体验
#### 实时过滤
用户输入时:
- **中文输入法支持**:在输入拼音时不会被过滤,等用户选择中文后才进行过滤
- **英文字母、数字、所有标点符号和特殊符号** 会被立即删除(用户无法输入)
- **敏感词** 会被替换为星号(例如:"垃圾" → "**")
- **中文字符** 可以正常输入
示例:
- 输入 "你好abc123世界" → 实时显示 "你好世界"
- 输入 "测试垃圾内容" → 实时显示 "测试**内容"
- 输入 "你好,世界!" → 实时显示 "你好世界"(标点被过滤)
- 使用中文输入法输入 "nihao" → 看到拼音 → 选择"你好" → 显示"你好" ✅
#### 提交校验
当用户点击提交时:
- 如果内容包含被禁止的字符或敏感词,会显示相应提示
- 错误的输入框会清空并标红
- 用户需要重新填写
#### 允许的内容
- ✅ 纯中文字符(汉字)
- ✅ 空格
- ❌ 英文字母(自动删除)
- ❌ 数字(自动删除)
- ❌ 所有标点符号(包括中文标点如,。!?;:等,自动删除)
- ❌ 所有特殊符号(@#$%^&*等,自动删除)
- ⚠️ 敏感词(替换为星号 * )
## 测试
所有校验函数都已通过测试:
- ✓ 英文字母检测和过滤
- ✓ 数字检测和过滤
- ✓ 特殊符号检测和过滤
- ✓ 敏感词检测和过滤
- ✓ 中文标点符号保留
- ✓ 完整的提交校验流程
## 使用示例
```typescript
import { filterInput, validateInput } from '@/lib/utils/sensitiveWords'
// 实时过滤
const handleChange = (value: string) => {
const filtered = filterInput(value) // 自动过滤英文、数字、特殊符号、敏感词
setValue(filtered)
}
// 提交前校验
const handleSubmit = () => {
const result = validateInput(value)
if (!result.valid) {
showHint(result.message)
return
}
// 继续提交逻辑
}
```
## 配置选项
在 `LetterFieldConfig` 中可以通过 `sanitize` 字段控制是否启用校验:
```typescript
const field: LetterFieldConfig = {
type: 'input',
sanitize: true, // 默认启用校验
// sanitize: false, // 禁用校验(不建议)
}
```
## 注意事项
1. **落款姓名** 也会进行相同的校验
2. **中文输入法支持** - 使用 `onCompositionStart` 和 `onCompositionEnd` 事件处理中文输入法,确保拼音输入过程不会被过滤
3. **敏感词库** 需要定期维护和更新
4. 如果需要添加更多敏感词,请在 `sensitiveWords.ts` 的 `SENSITIVE_WORDS` 数组中添加
5. 所有标点符号(包括中文标点)都会被过滤
+3
View File
@@ -8,6 +8,7 @@
"axios": "^1.12.2", "axios": "^1.12.2",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"framer-motion": "^12.23.26", "framer-motion": "^12.23.26",
"html-to-image": "^1.11.13",
"lucide-react": "^0.545.0", "lucide-react": "^0.545.0",
"mammoth": "^1.11.0", "mammoth": "^1.11.0",
"react": "^19.1.1", "react": "^19.1.1",
@@ -621,6 +622,8 @@
"hasown": ["hasown@2.0.2", "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], "hasown": ["hasown@2.0.2", "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
"html-to-image": ["html-to-image@1.11.13", "https://registry.npmmirror.com/html-to-image/-/html-to-image-1.11.13.tgz", {}, "sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg=="],
"ignore": ["ignore@7.0.5", "https://registry.npmmirror.com/ignore/-/ignore-7.0.5.tgz", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], "ignore": ["ignore@7.0.5", "https://registry.npmmirror.com/ignore/-/ignore-7.0.5.tgz", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
"immediate": ["immediate@3.0.6", "https://registry.npmmirror.com/immediate/-/immediate-3.0.6.tgz", {}, "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="], "immediate": ["immediate@3.0.6", "https://registry.npmmirror.com/immediate/-/immediate-3.0.6.tgz", {}, "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="],
+1 -1
View File
@@ -1,4 +1,4 @@
<!DOCTYPE html> <!doctype html>
<html lang="zh-CN"> <html lang="zh-CN">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
+56
View File
@@ -8,6 +8,9 @@
"name": "shanghai-residences", "name": "shanghai-residences",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"axios": "^1.12.2", "axios": "^1.12.2",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"framer-motion": "^12.23.26", "framer-motion": "^12.23.26",
@@ -339,6 +342,59 @@
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@dnd-kit/accessibility": {
"version": "3.1.1",
"resolved": "https://registry.npmmirror.com/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
"integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==",
"license": "MIT",
"dependencies": {
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@dnd-kit/core": {
"version": "6.3.1",
"resolved": "https://registry.npmmirror.com/@dnd-kit/core/-/core-6.3.1.tgz",
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
"license": "MIT",
"dependencies": {
"@dnd-kit/accessibility": "^3.1.1",
"@dnd-kit/utilities": "^3.2.2",
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/@dnd-kit/sortable": {
"version": "10.0.0",
"resolved": "https://registry.npmmirror.com/@dnd-kit/sortable/-/sortable-10.0.0.tgz",
"integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==",
"license": "MIT",
"dependencies": {
"@dnd-kit/utilities": "^3.2.2",
"tslib": "^2.0.0"
},
"peerDependencies": {
"@dnd-kit/core": "^6.3.0",
"react": ">=16.8.0"
}
},
"node_modules/@dnd-kit/utilities": {
"version": "3.2.2",
"resolved": "https://registry.npmmirror.com/@dnd-kit/utilities/-/utilities-3.2.2.tgz",
"integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==",
"license": "MIT",
"dependencies": {
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@esbuild/aix-ppc64": { "node_modules/@esbuild/aix-ppc64": {
"version": "0.27.3", "version": "0.27.3",
"resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz",
+4 -2
View File
@@ -8,8 +8,10 @@
"bun": ">=1.0.0" "bun": ">=1.0.0"
}, },
"scripts": { "scripts": {
"dev": "vite", "dev": "vite --mode development",
"build": "tsc && vite build", "dev:test": "vite --mode test",
"build": "tsc && vite build --mode production",
"build:test": "tsc && vite build --mode test",
"lint": "eslint .", "lint": "eslint .",
"lint:fix": "eslint . --fix", "lint:fix": "eslint . --fix",
"format": "prettier --write .", "format": "prettier --write .",
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+4901 -1
View File
File diff suppressed because it is too large. Load diff
+281 -231
View File
@@ -8,145 +8,157 @@
* http://en.wikipedia.org/wiki/Base64 * http://en.wikipedia.org/wiki/Base64
*/ */
(function(global) { ;(function (global) {
'use strict'; 'use strict'
// existing version for noConflict() // existing version for noConflict()
var _Base64 = global.Base64; var _Base64 = global.Base64
var version = "2.1.5"; var version = '2.1.5'
// if node.js, we use Buffer // if node.js, we use Buffer
var buffer; var buffer
if (typeof module !== 'undefined' && module.exports) { if (typeof module !== 'undefined' && module.exports) {
buffer = require('buffer').Buffer; buffer = require('buffer').Buffer
} }
// constants // constants
var b64chars var b64chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
= 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; var b64tab = (function (bin) {
var b64tab = function(bin) { var t = {}
var t = {}; for (var i = 0, l = bin.length; i < l; i++) t[bin.charAt(i)] = i
for (var i = 0, l = bin.length; i < l; i++) t[bin.charAt(i)] = i; return t
return t; })(b64chars)
}(b64chars); var fromCharCode = String.fromCharCode
var fromCharCode = String.fromCharCode;
// encoder stuff // encoder stuff
var cb_utob = function(c) { var cb_utob = function (c) {
if (c.length < 2) { if (c.length < 2) {
var cc = c.charCodeAt(0); var cc = c.charCodeAt(0)
return cc < 0x80 ? c return cc < 0x80
: cc < 0x800 ? (fromCharCode(0xc0 | (cc >>> 6)) ? c
+ fromCharCode(0x80 | (cc & 0x3f))) : cc < 0x800
: (fromCharCode(0xe0 | ((cc >>> 12) & 0x0f)) ? fromCharCode(0xc0 | (cc >>> 6)) + fromCharCode(0x80 | (cc & 0x3f))
+ fromCharCode(0x80 | ((cc >>> 6) & 0x3f)) : fromCharCode(0xe0 | ((cc >>> 12) & 0x0f)) +
+ fromCharCode(0x80 | ( cc & 0x3f))); fromCharCode(0x80 | ((cc >>> 6) & 0x3f)) +
fromCharCode(0x80 | (cc & 0x3f))
} else { } else {
var cc = 0x10000 var cc = 0x10000 + (c.charCodeAt(0) - 0xd800) * 0x400 + (c.charCodeAt(1) - 0xdc00)
+ (c.charCodeAt(0) - 0xD800) * 0x400 return (
+ (c.charCodeAt(1) - 0xDC00); fromCharCode(0xf0 | ((cc >>> 18) & 0x07)) +
return (fromCharCode(0xf0 | ((cc >>> 18) & 0x07)) fromCharCode(0x80 | ((cc >>> 12) & 0x3f)) +
+ fromCharCode(0x80 | ((cc >>> 12) & 0x3f)) fromCharCode(0x80 | ((cc >>> 6) & 0x3f)) +
+ fromCharCode(0x80 | ((cc >>> 6) & 0x3f)) fromCharCode(0x80 | (cc & 0x3f))
+ fromCharCode(0x80 | ( cc & 0x3f))); )
} }
}; }
var re_utob = /[\uD800-\uDBFF][\uDC00-\uDFFFF]|[^\x00-\x7F]/g; var re_utob = /[\uD800-\uDBFF][\uDC00-\uDFFFF]|[^\x00-\x7F]/g
var utob = function(u) { var utob = function (u) {
return u.replace(re_utob, cb_utob); return u.replace(re_utob, cb_utob)
}; }
var cb_encode = function(ccc) { var cb_encode = function (ccc) {
var padlen = [0, 2, 1][ccc.length % 3], var padlen = [0, 2, 1][ccc.length % 3],
ord = ccc.charCodeAt(0) << 16 ord =
| ((ccc.length > 1 ? ccc.charCodeAt(1) : 0) << 8) (ccc.charCodeAt(0) << 16) |
| ((ccc.length > 2 ? ccc.charCodeAt(2) : 0)), ((ccc.length > 1 ? ccc.charCodeAt(1) : 0) << 8) |
(ccc.length > 2 ? ccc.charCodeAt(2) : 0),
chars = [ chars = [
b64chars.charAt( ord >>> 18), b64chars.charAt(ord >>> 18),
b64chars.charAt((ord >>> 12) & 63), b64chars.charAt((ord >>> 12) & 63),
padlen >= 2 ? '=' : b64chars.charAt((ord >>> 6) & 63), padlen >= 2 ? '=' : b64chars.charAt((ord >>> 6) & 63),
padlen >= 1 ? '=' : b64chars.charAt(ord & 63) padlen >= 1 ? '=' : b64chars.charAt(ord & 63),
]; ]
return chars.join(''); return chars.join('')
}; }
var btoa = global.btoa ? function(b) { var btoa = global.btoa
return global.btoa(b); ? function (b) {
} : function(b) { return global.btoa(b)
return b.replace(/[\s\S]{1,3}/g, cb_encode); }
}; : function (b) {
return b.replace(/[\s\S]{1,3}/g, cb_encode)
}
var _encode = buffer var _encode = buffer
? function (u) { return (new buffer(u)).toString('base64') } ? function (u) {
: function (u) { return btoa(utob(u)) } return new buffer(u).toString('base64')
; }
var encode = function(u, urisafe) { : function (u) {
return btoa(utob(u))
}
var encode = function (u, urisafe) {
return !urisafe return !urisafe
? _encode(u) ? _encode(u)
: _encode(u).replace(/[+\/]/g, function(m0) { : _encode(u)
return m0 == '+' ? '-' : '_'; .replace(/[+\/]/g, function (m0) {
}).replace(/=/g, ''); return m0 == '+' ? '-' : '_'
}; })
var encodeURI = function(u) { return encode(u, true) }; .replace(/=/g, '')
}
var encodeURI = function (u) {
return encode(u, true)
}
// decoder stuff // decoder stuff
var re_btou = new RegExp([ var re_btou = new RegExp(
'[\xC0-\xDF][\x80-\xBF]', ['[\xC0-\xDF][\x80-\xBF]', '[\xE0-\xEF][\x80-\xBF]{2}', '[\xF0-\xF7][\x80-\xBF]{3}'].join('|'),
'[\xE0-\xEF][\x80-\xBF]{2}', 'g'
'[\xF0-\xF7][\x80-\xBF]{3}' )
].join('|'), 'g'); var cb_btou = function (cccc) {
var cb_btou = function(cccc) { switch (cccc.length) {
switch(cccc.length) {
case 4: case 4:
var cp = ((0x07 & cccc.charCodeAt(0)) << 18) var cp =
| ((0x3f & cccc.charCodeAt(1)) << 12) ((0x07 & cccc.charCodeAt(0)) << 18) |
| ((0x3f & cccc.charCodeAt(2)) << 6) ((0x3f & cccc.charCodeAt(1)) << 12) |
| (0x3f & cccc.charCodeAt(3)), ((0x3f & cccc.charCodeAt(2)) << 6) |
offset = cp - 0x10000; (0x3f & cccc.charCodeAt(3)),
return (fromCharCode((offset >>> 10) + 0xD800) offset = cp - 0x10000
+ fromCharCode((offset & 0x3FF) + 0xDC00)); return fromCharCode((offset >>> 10) + 0xd800) + fromCharCode((offset & 0x3ff) + 0xdc00)
case 3: case 3:
return fromCharCode( return fromCharCode(
((0x0f & cccc.charCodeAt(0)) << 12) ((0x0f & cccc.charCodeAt(0)) << 12) |
| ((0x3f & cccc.charCodeAt(1)) << 6) ((0x3f & cccc.charCodeAt(1)) << 6) |
| (0x3f & cccc.charCodeAt(2)) (0x3f & cccc.charCodeAt(2))
); )
default: default:
return fromCharCode( return fromCharCode(((0x1f & cccc.charCodeAt(0)) << 6) | (0x3f & cccc.charCodeAt(1)))
((0x1f & cccc.charCodeAt(0)) << 6)
| (0x3f & cccc.charCodeAt(1))
);
} }
}; }
var btou = function(b) { var btou = function (b) {
return b.replace(re_btou, cb_btou); return b.replace(re_btou, cb_btou)
}; }
var cb_decode = function(cccc) { var cb_decode = function (cccc) {
var len = cccc.length, var len = cccc.length,
padlen = len % 4, padlen = len % 4,
n = (len > 0 ? b64tab[cccc.charAt(0)] << 18 : 0) n =
| (len > 1 ? b64tab[cccc.charAt(1)] << 12 : 0) (len > 0 ? b64tab[cccc.charAt(0)] << 18 : 0) |
| (len > 2 ? b64tab[cccc.charAt(2)] << 6 : 0) (len > 1 ? b64tab[cccc.charAt(1)] << 12 : 0) |
| (len > 3 ? b64tab[cccc.charAt(3)] : 0), (len > 2 ? b64tab[cccc.charAt(2)] << 6 : 0) |
chars = [ (len > 3 ? b64tab[cccc.charAt(3)] : 0),
fromCharCode( n >>> 16), chars = [fromCharCode(n >>> 16), fromCharCode((n >>> 8) & 0xff), fromCharCode(n & 0xff)]
fromCharCode((n >>> 8) & 0xff), chars.length -= [0, 0, 2, 1][padlen]
fromCharCode( n & 0xff) return chars.join('')
]; }
chars.length -= [0, 0, 2, 1][padlen]; var atob = global.atob
return chars.join(''); ? function (a) {
}; return global.atob(a)
var atob = global.atob ? function(a) { }
return global.atob(a); : function (a) {
} : function(a){ return a.replace(/[\s\S]{1,4}/g, cb_decode)
return a.replace(/[\s\S]{1,4}/g, cb_decode); }
};
var _decode = buffer var _decode = buffer
? function(a) { return (new buffer(a, 'base64')).toString() } ? function (a) {
: function(a) { return btou(atob(a)) }; return new buffer(a, 'base64').toString()
var decode = function(a){ }
: function (a) {
return btou(atob(a))
}
var decode = function (a) {
return _decode( return _decode(
a.replace(/[-_]/g, function(m0) { return m0 == '-' ? '+' : '/' }) a
.replace(/[-_]/g, function (m0) {
return m0 == '-' ? '+' : '/'
})
.replace(/[^A-Za-z0-9\+\/]/g, '') .replace(/[^A-Za-z0-9\+\/]/g, '')
); )
}; }
var noConflict = function() { var noConflict = function () {
var Base64 = global.Base64; var Base64 = global.Base64
global.Base64 = _Base64; global.Base64 = _Base64
return Base64; return Base64
}; }
// export Base64 // export Base64
global.Base64 = { global.Base64 = {
VERSION: version, VERSION: version,
@@ -159,57 +171,66 @@
encodeURI: encodeURI, encodeURI: encodeURI,
btou: btou, btou: btou,
decode: decode, decode: decode,
noConflict: noConflict noConflict: noConflict,
}; }
// if ES5 is available, make Base64.extendString() available // if ES5 is available, make Base64.extendString() available
if (typeof Object.defineProperty === 'function') { if (typeof Object.defineProperty === 'function') {
var noEnum = function(v){ var noEnum = function (v) {
return {value:v,enumerable:false,writable:true,configurable:true}; return { value: v, enumerable: false, writable: true, configurable: true }
}; }
global.Base64.extendString = function () { global.Base64.extendString = function () {
Object.defineProperty( Object.defineProperty(
String.prototype, 'fromBase64', noEnum(function () { String.prototype,
'fromBase64',
noEnum(function () {
return decode(this) return decode(this)
})); })
)
Object.defineProperty( Object.defineProperty(
String.prototype, 'toBase64', noEnum(function (urisafe) { String.prototype,
'toBase64',
noEnum(function (urisafe) {
return encode(this, urisafe) return encode(this, urisafe)
})); })
)
Object.defineProperty( Object.defineProperty(
String.prototype, 'toBase64URI', noEnum(function () { String.prototype,
'toBase64URI',
noEnum(function () {
return encode(this, true) return encode(this, true)
})); })
}; )
}
} }
// that's it! // that's it!
})(this); })(this)
if (this['Meteor']) { if (this['Meteor']) {
Base64 = global.Base64; // for normal export in Meteor.js Base64 = global.Base64 // for normal export in Meteor.js
} }
; (function (root) { ;(function (root) {
// body... // body...
var videoajax = function (argument) { var videoajax = function (argument) {
// body... // body...
var host = '/'; var host = '/'
this._setHost=function(hoststr) { this._setHost = function (hoststr) {
host = hoststr; host = hoststr
}; }
this._getHost = function(){ this._getHost = function () {
return host; return host
}
} }
};
$.extend(videoajax.prototype,{ $.extend(videoajax.prototype, {
/** /**
* 设置主域名 * 设置主域名
* @param hosts {String} default: '/' 主域名字符串 * @param hosts {String} default: '/' 主域名字符串
*/ */
setHost :function(hosts){ setHost: function (hosts) {
this._setHost(hosts); this._setHost(hosts)
}, },
//user_id,course_id,lesson_id,break_point //user_id,course_id,lesson_id,break_point
/** /**
@@ -221,15 +242,18 @@ if (this['Meteor']) {
* break_point {Number} 断点 * break_point {Number} 断点
*@param callback {Function} 回调方法 *@param callback {Function} 回调方法
*/ */
quitBreakPoint:function(data,callback){ quitBreakPoint: function (data, callback) {
var paramfiled = ["user_id","course_id","lesson_id","break_point"]; var paramfiled = ['user_id', 'course_id', 'lesson_id', 'break_point']
var dataStr = verfiyData(paramfiled,data); var dataStr = verfiyData(paramfiled, data)
getDate(this._getHost()+'index.php?m=zzplatform&c=userview&a=addBreakPoint',{ getDate(
userViewPoint:dataStr this._getHost() + 'index.php?m=zzplatform&c=userview&a=addBreakPoint',
},function(data){ {
isFunction(callback) ? callback(data.code == 200,data) : ''; userViewPoint: dataStr,
},
function (data) {
isFunction(callback) ? callback(data.code == 200, data) : ''
} }
); )
}, },
/** /**
* user_id:123,course_id:23,lesson_id:3 * user_id:123,course_id:23,lesson_id:3
@@ -240,16 +264,19 @@ if (this['Meteor']) {
* lesson_id {Number} 课时ID * lesson_id {Number} 课时ID
*@param callback {Function} 回调方法 *@param callback {Function} 回调方法
*/ */
getBreakPoint:function(data,callback){ getBreakPoint: function (data, callback) {
var paramfiled = ["user_id","course_id","lesson_id"]; var paramfiled = ['user_id', 'course_id', 'lesson_id']
var dataStr = verfiyData(paramfiled,data); var dataStr = verfiyData(paramfiled, data)
getDate(this._getHost()+'index.php?m=zzplatform&c=userview&a=userbreakpoint',{ getDate(
userCourseInfo:dataStr this._getHost() + 'index.php?m=zzplatform&c=userview&a=userbreakpoint',
},function(data){ {
isFunction(callback) ? callback(data.code == 200,data) : ''; userCourseInfo: dataStr,
},
function (data) {
isFunction(callback) ? callback(data.code == 200, data) : ''
// isFunction(callback) ? callback(data) : ''; // isFunction(callback) ? callback(data) : '';
} }
); )
}, },
/** /**
*用户答题答案 *用户答题答案
@@ -261,16 +288,18 @@ if (this['Meteor']) {
* answer {String} 问题 * answer {String} 问题
*@param callback {Function} 回调方法 *@param callback {Function} 回调方法
*/ */
getUserAnswer:function(data,callback){ getUserAnswer: function (data, callback) {
var paramfiled = ["user_id","lesson_id","course_id","question_order","answer"]; var paramfiled = ['user_id', 'lesson_id', 'course_id', 'question_order', 'answer']
var dataStr = verfiyData(paramfiled,data); var dataStr = verfiyData(paramfiled, data)
getDate(this._getHost()+'index.php?m=zzplatform&c=userview&a=addUserAnswer',{ getDate(
userAnswerInfo:dataStr this._getHost() + 'index.php?m=zzplatform&c=userview&a=addUserAnswer',
},function(data){ {
isFunction(callback) ? callback(data.code == 200,data) : ''; userAnswerInfo: dataStr,
},
function (data) {
isFunction(callback) ? callback(data.code == 200, data) : ''
} }
); )
}, },
/** /**
*播放结束 *播放结束
@@ -279,14 +308,17 @@ if (this['Meteor']) {
* co {Number} 课时ID * co {Number} 课时ID
*@param callback {Function} 回调方法 *@param callback {Function} 回调方法
*/ */
finishCourse:function(data,callback){ finishCourse: function (data, callback) {
var paramfiled = ["order","co","process"]; var paramfiled = ['order', 'co', 'process']
var dataStr = verfiyData(paramfiled,data); var dataStr = verfiyData(paramfiled, data)
getDate(this._getHost()+'index.php?m=user&c=learnrecord&a=playcount',data,function(data){ getDate(
isFunction(callback) ? callback(data.code == 200,data) : ''; this._getHost() + 'index.php?m=user&c=learnrecord&a=playcount',
data,
function (data) {
isFunction(callback) ? callback(data.code == 200, data) : ''
// isFunction(callback) ? callback(data) : ''; // isFunction(callback) ? callback(data) : '';
} }
); )
}, },
/** /**
*课程进度获取 *课程进度获取
@@ -299,21 +331,24 @@ if (this['Meteor']) {
* 其中status为true为调用正常, 如果有值则data返回json形式的进度, 没有值则返回[] * 其中status为true为调用正常, 如果有值则data返回json形式的进度, 没有值则返回[]
* status为false时, data保存的是错误提示 * status为false时, data保存的是错误提示
*/ */
getfinishCourse:function(data,callback){ getfinishCourse: function (data, callback) {
var paramfiled = ["order","co"]; var paramfiled = ['order', 'co']
var dataStr = verfiyData(paramfiled,data); var dataStr = verfiyData(paramfiled, data)
getDate(this._getHost()+'index.php?m=user&c=learnrecord&a=getuserlessionprocess',data,function(data){ getDate(
isFunction(callback) ? callback(data) : ''; this._getHost() + 'index.php?m=user&c=learnrecord&a=getuserlessionprocess',
data,
function (data) {
isFunction(callback) ? callback(data) : ''
// isFunction(callback) ? callback(data) : ''; // isFunction(callback) ? callback(data) : '';
} }
); )
}, },
/** /**
*解码加密的breakpoint *解码加密的breakpoint
* @pparam basecode {String} 要解码的字符串 * @pparam basecode {String} 要解码的字符串
*/ */
fromBaseCode : function(basecode){ fromBaseCode: function (basecode) {
return getBreakPoint(basecode); return getBreakPoint(basecode)
}, },
/** /**
*通过key值和课时值获取自定的数据 *通过key值和课时值获取自定的数据
@@ -322,14 +357,18 @@ if (this['Meteor']) {
* order {Number} 排序ID * order {Number} 排序ID
*@param callback {Function} 回调方法 *@param callback {Function} 回调方法
*/ */
getUserDefineInfo :function(data,callback){ getUserDefineInfo: function (data, callback) {
var paramfiled = ["lessonId","order"]; var paramfiled = ['lessonId', 'order']
var dataStr = verfiyData(paramfiled,data); var dataStr = verfiyData(paramfiled, data)
getDate(this._getHost()+'index.php?m=zzplatform&c=userview&a=getUserDefineInfo',{ getDate(
defineDate:dataStr this._getHost() + 'index.php?m=zzplatform&c=userview&a=getUserDefineInfo',
},function(data){ {
isFunction(callback) ? callback(data) : ''; defineDate: dataStr,
}); },
function (data) {
isFunction(callback) ? callback(data) : ''
}
)
}, },
/** /**
*通过key值和课时值及用户ID获取自定的数据 *通过key值和课时值及用户ID获取自定的数据
@@ -338,14 +377,18 @@ if (this['Meteor']) {
* order {Number} 排序ID * order {Number} 排序ID
*@param callback {Function} 回调方法 *@param callback {Function} 回调方法
*/ */
getUserDefineInfoByUser :function(data,callback){ getUserDefineInfoByUser: function (data, callback) {
var paramfiled = ["lessonId","order"]; var paramfiled = ['lessonId', 'order']
var dataStr = verfiyData(paramfiled,data); var dataStr = verfiyData(paramfiled, data)
getDate(this._getHost()+'index.php?m=zzplatform&c=userview&a=getUserDefineInfoByUser',{ getDate(
defineUserDate:dataStr this._getHost() + 'index.php?m=zzplatform&c=userview&a=getUserDefineInfoByUser',
},function(data){ {
isFunction(callback) ? callback(data) : ''; defineUserDate: dataStr,
}); },
function (data) {
isFunction(callback) ? callback(data) : ''
}
)
}, },
/** /**
*增加自定数据 *增加自定数据
@@ -355,53 +398,60 @@ if (this['Meteor']) {
* content {string} 字内容 * content {string} 字内容
*@param callback {Function} 回调方法 *@param callback {Function} 回调方法
*/ */
addUserDefine:function(data,callback){ addUserDefine: function (data, callback) {
var paramfiled = ["lessonId","key","content"]; var paramfiled = ['lessonId', 'key', 'content']
var dataStr = verfiyData(paramfiled,data); var dataStr = verfiyData(paramfiled, data)
getDate(this._getHost()+'index.php?m=zzplatform&c=userview&a=addUserDefine',{ getDate(
addDefineDate:dataStr this._getHost() + 'index.php?m=zzplatform&c=userview&a=addUserDefine',
},function(data){ {
isFunction(callback) ? callback(data) : ''; addDefineDate: dataStr,
}); },
function (data) {
isFunction(callback) ? callback(data) : ''
} }
)
},
}) })
var getDate = function(url,data,callback){ var getDate = function (url, data, callback) {
$.get(url,data,function(data){ $.get(
callback(data); url,
},'json') data,
}; function (data) {
callback(data)
var isFunction = function(fn){ },
if(typeof fn === 'function')return true; 'json'
return false; )
}
var verfiyData = function(field,data){
var arr=[];
console.log(field);
console.log('<br/>');
console.log(data);
for(var i in field){
if(!data[field[i]]){
throw new Error('data is error');
}
arr.push(field[i]+':'+data[field[i]]);
}
if(root.JSON){
return JSON.stringify(data);
}else{
return '{'+arr.join(',')+'}';
} }
var isFunction = function (fn) {
if (typeof fn === 'function') return true
return false
}
var verfiyData = function (field, data) {
var arr = []
console.log(field)
console.log('<br/>')
console.log(data)
for (var i in field) {
if (!data[field[i]]) {
throw new Error('data is error')
}
arr.push(field[i] + ':' + data[field[i]])
}
if (root.JSON) {
return JSON.stringify(data)
} else {
return '{' + arr.join(',') + '}'
}
} }
var getBreakPoint = function (base64point) { var getBreakPoint = function (base64point) {
var newbc = base64point.split('').reverse().join(''); var newbc = base64point.split('').reverse().join('')
return Base64.fromBase64(newbc); return Base64.fromBase64(newbc)
} }
root.videoAjax = function(){ root.videoAjax = function () {
return new videoajax(); return new videoajax()
} }
}(this)); })(this)
+45 -17
View File
@@ -1,17 +1,24 @@
import { RouterProvider } from 'react-router-dom' import { RouterProvider } from 'react-router-dom'
import { Suspense, useEffect } from 'react' import { Suspense, useEffect, useState } from 'react'
import router from './router' import router from './router'
import { ScaleProvider, useScale } from './contexts/ScaleContext' import { ScaleProvider, useScale } from './contexts/ScaleContext'
import { RotateHint } from './components/RotateHint' import { RotateHint } from './components/RotateHint'
import { HintHost } from './components' import { HintHost } from './components'
import { useUserStore } from './store' import { useUserStore } from './store'
import { getUserDefine } from '@/lib/utils/api-common'
import { import {
usePuzzleStore, SyncLoadingHost,
deserializePuzzles, SyncPromptHost,
PUZZLE_LESSON_ID, showYibanSyncErrorPrompt,
PUZZLE_KEY, showYibanUnauthenticatedPrompt,
} from '@/store/puzzleStore' } from '@/components'
import { usePuzzleStore, loadPuzzleStore } from '@/store/puzzleStore'
const applyStoredProgressionDebugMode = async () => {
if (import.meta.env.VITE_DEBUG_ENTRY !== 'true') return false
const debug = await import('@/debug/progressionDebug')
return debug.applyStoredProgressionDebugMode()
}
/** /**
* 应用内容组件 * 应用内容组件
@@ -19,11 +26,12 @@ import {
*/ */
function AppContent() { function AppContent() {
const { scaleStyle } = useScale() const { scaleStyle } = useScale()
const [bootstrapDone, setBootstrapDone] = useState(false)
const setCourseInfo = useUserStore((state) => state.setCourseInfo) const setCourseInfo = useUserStore((state) => state.setCourseInfo)
useEffect(() => { useEffect(() => {
const params = new URLSearchParams(window.location.search) const params = new URLSearchParams(window.location.search)
const courseId = params.get('courseid') const courseId = params.get('courseId') ?? params.get('courseid')
const uid = params.get('uid') const uid = params.get('uid')
if (courseId || uid) { if (courseId || uid) {
@@ -37,17 +45,35 @@ function AppContent() {
useEffect(() => { useEffect(() => {
;(async () => { ;(async () => {
try { try {
const raw = await getUserDefine(PUZZLE_LESSON_ID, PUZZLE_KEY) const status = await loadPuzzleStore({ backend: 'yiban' })
if (!raw) return
const data = JSON.parse(raw) as { puzzles?: Record<string, number[]> } if (status === 'success' || status === 'empty') {
if (data.puzzles) { useUserStore.getState().setYibanConnectionStatus('connected')
usePuzzleStore.setState((prev) => ({ return
...prev, }
puzzles: deserializePuzzles(data.puzzles),
})) useUserStore.getState().setYibanConnectionStatus('disconnected')
await loadPuzzleStore({ backend: 'session' })
if (status === 'unauthenticated') {
showYibanUnauthenticatedPrompt()
} else if (status === 'error') {
showYibanSyncErrorPrompt()
} }
} catch (e) { } catch (e) {
useUserStore.getState().setYibanConnectionStatus('disconnected')
await loadPuzzleStore({ backend: 'session' })
showYibanSyncErrorPrompt()
console.error('Failed to load puzzle store', e) console.error('Failed to load puzzle store', e)
} finally {
await applyStoredProgressionDebugMode()
// 仅在测试包启用时应用阶段覆盖(防止 sessionStorage 残留污染生产数据)
if (import.meta.env.VITE_DEBUG_ENTRY === 'true') {
const { applyStoredStageOverrides } = await import('@/debug/progressionDebug')
applyStoredStageOverrides()
}
usePuzzleStore.getState().setLoaded(true)
setBootstrapDone(true)
} }
})() })()
}, []) }, [])
@@ -62,9 +88,11 @@ function AppContent() {
}} }}
> >
<Suspense fallback={<div>Loading...</div>}> <Suspense fallback={<div>Loading...</div>}>
<RouterProvider router={router} /> {bootstrapDone && <RouterProvider router={router} />}
<HintHost /> <HintHost />
<SyncPromptHost />
</Suspense> </Suspense>
<SyncLoadingHost visible={!bootstrapDone} />
</div> </div>
</div> </div>
<RotateHint /> <RotateHint />
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 MiB

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 928 KiB

After

Width:  |  Height:  |  Size: 920 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 665 KiB

After

Width:  |  Height:  |  Size: 664 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 MiB

After

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 422 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1003 KiB

After

Width:  |  Height:  |  Size: 907 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 933 KiB

After

Width:  |  Height:  |  Size: 933 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 544 KiB

After

Width:  |  Height:  |  Size: 567 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 MiB

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 439 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 398 KiB

Loaded 100 of 422 files, more files were not shown because too many files have changed in this diff. Show more