fix: 封面图 HTTP → HTTPS 绕过 ATS 限制;每页页面标题独立;无账号提示本地化修复

- 封面图加载失败:阿里云 VOD CoverURL 返回 HTTP,ATS 默认禁止,替换为 HTTPS
- 签名编码优化:percentEncodeURL 不编码逗号,URL 中 Fields 参数保持原样
- 每个页面独立 navigationTitle(视频库/下载任务/账号配置/操作日志)
- 无账号时 ContentUnavailableView 显示本地化提示
- 每页条数选择器宽度 80→120,避免文字截断
This commit is contained in:
2026-06-30 16:27:34 +06:00
parent d94054dd1a
commit 7ec5cee7b0
9 changed files with 131 additions and 12 deletions
+67
View File
@@ -0,0 +1,67 @@
# 隐私政策
最后更新日期:2026 年 6 月 30 日
感谢您使用 VOD 视频管理器(以下简称"本应用")。本应用尊重并保护您的隐私。
## 数据收集
本应用 **不会收集、存储或上传任何个人数据**
- 本应用不包含任何分析服务、广告 SDK 或第三方跟踪代码。
- 本应用不向第三方服务器发送任何用户数据。
- 本应用无需注册账户即可使用。
## 用户输入的数据
### 阿里云 AccessKey
使用本应用时,您需要自行输入阿里云的 AccessKey ID 和 AccessKey Secret。这些信息:
- **仅存储在您的本地设备上**(通过 SwiftData 本地数据库)。
- **不会传输到任何第三方服务器**。
- 仅用于直接访问阿里云视频点播(VOD)OpenAPI。
- 您可以随时在应用中删除这些信息。
### 视频数据
本应用从阿里云 VOD 服务获取的视频列表、播放地址等信息:
- **仅存储在您的本地设备上**。
- 不会上传到任何第三方服务。
## 网络通信
本应用仅与 **阿里云 VOD API**`vod.{region}.aliyuncs.com`)直接通信,用于执行您主动发起的操作,例如:
- 拉取视频列表和详情
- 获取播放地址
- 删除视频
- 下载视频文件
所有网络请求均通过 HTTPS 加密传输。
## 数据删除
您可以通过以下方式删除所有本地数据:
- 在应用中删除账号配置。
- 删除应用的本地数据库文件。
- 卸载本应用。
## 第三方服务
本应用使用了以下系统框架和服务:
- **SwiftData**:本地数据持久化(SQLite
- **URLSession**:网络请求(仅连接阿里云 API)
## 隐私政策更新
如果本隐私政策发生变更,我们会在本页面更新。继续使用本应用即表示您同意更新后的条款。
## 联系我们
如有任何关于隐私政策的问题,请通过以下方式联系:
- GitHub[https://github.com/anomalyco/Aliyun-VOD-Media-Library-Manager](https://github.com/anomalyco/Aliyun-VOD-Media-Library-Manager)
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>SchemeUserState</key>
<dict>
<key>VODManager.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>0</integer>
</dict>
</dict>
</dict>
</plist>
@@ -1267,7 +1267,26 @@
}
},
"error.no_active_account" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "No Active Account"
}
},
"ug" : {
"stringUnit" : {
"state" : "translated",
"value" : "ھەساب يوق"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "没有激活的账号"
}
}
}
},
"error.no_play_url" : {
"localizations" : {
@@ -67,7 +67,7 @@ actor VODClient {
let host = endpoint()
let query = params.sorted(by: { $0.key < $1.key })
.map { "\(VODSignature.percentEncodePublic($0.key))=\(VODSignature.percentEncodePublic($0.value))" }
.map { "\(VODSignature.percentEncodeURL($0.key))=\(VODSignature.percentEncodeURL($0.value))" }
.joined(separator: "&")
let urlString = "https://\(host)/?\(query)"
@@ -43,6 +43,19 @@ struct VODSignature {
percentEncode(value)
}
static func percentEncodeURL(_ value: String) -> String {
let allowed = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~,"
var result = ""
for char in value.utf8 {
if allowed.utf8.contains(char) {
result.append(Character(UnicodeScalar(char)))
} else {
result.append(String(format: "%%%02X", char))
}
}
return result
}
private static func percentEncode(_ value: String) -> String {
let allowed = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~"
var result = ""
@@ -79,6 +79,7 @@ struct AccountListView: View {
.sheet(isPresented: $showAddForm) {
AccountFormView(account: nil)
}
.navigationTitle("nav.account_config")
}
}
@@ -26,5 +26,6 @@ struct LogListView: View {
}
.padding(.vertical, 4)
}
.navigationTitle("nav.operation_logs")
}
}
@@ -20,16 +20,19 @@ struct VideoListView: View {
private var totalPages: Int { max(1, (filteredVideos.count + pageSize - 1) / pageSize) }
var body: some View {
Group {
if accountStore.activeAccount == nil {
ContentUnavailableView(
"error.no_active_account",
systemImage: "person.slash",
description: Text("account.add")
)
ContentUnavailableView {
Label("error.no_active_account", systemImage: "person.slash")
} description: {
Text("account.add")
}
} else {
content
}
}
.navigationTitle("nav.video_library")
}
@ViewBuilder
private var content: some View {
@@ -126,7 +129,8 @@ struct VideoListView: View {
TableColumn("video.table.title") { video in
HStack {
AsyncImage(url: video.coverURL.flatMap(URL.init)) { image in
let coverURL = video.coverURL?.replacingOccurrences(of: "http://", with: "https://")
AsyncImage(url: coverURL.flatMap(URL.init)) { image in
image.resizable().scaledToFill()
} placeholder: {
Color.gray
@@ -192,7 +196,7 @@ struct VideoListView: View {
Text("\(pageSizes[i])").tag(i)
}
}
.frame(width: 80)
.frame(width: 120)
.onChange(of: pageSizeIndex) { _, _ in
pageNo = 1
updateDisplay()