fix: 封面图 HTTP → HTTPS 绕过 ATS 限制;每页页面标题独立;无账号提示本地化修复
- 封面图加载失败:阿里云 VOD CoverURL 返回 HTTP,ATS 默认禁止,替换为 HTTPS - 签名编码优化:percentEncodeURL 不编码逗号,URL 中 Fields 参数保持原样 - 每个页面独立 navigationTitle(视频库/下载任务/账号配置/操作日志) - 无账号时 ContentUnavailableView 显示本地化提示 - 每页条数选择器宽度 80→120,避免文字截断
This commit is contained in:
@@ -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)
|
||||
BIN
Binary file not shown.
+14
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user