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" : {
|
"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" : {
|
"error.no_play_url" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ actor VODClient {
|
|||||||
|
|
||||||
let host = endpoint()
|
let host = endpoint()
|
||||||
let query = params.sorted(by: { $0.key < $1.key })
|
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: "&")
|
.joined(separator: "&")
|
||||||
let urlString = "https://\(host)/?\(query)"
|
let urlString = "https://\(host)/?\(query)"
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,19 @@ struct VODSignature {
|
|||||||
percentEncode(value)
|
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 {
|
private static func percentEncode(_ value: String) -> String {
|
||||||
let allowed = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~"
|
let allowed = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~"
|
||||||
var result = ""
|
var result = ""
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ struct AccountListView: View {
|
|||||||
.sheet(isPresented: $showAddForm) {
|
.sheet(isPresented: $showAddForm) {
|
||||||
AccountFormView(account: nil)
|
AccountFormView(account: nil)
|
||||||
}
|
}
|
||||||
|
.navigationTitle("nav.account_config")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,5 +26,6 @@ struct LogListView: View {
|
|||||||
}
|
}
|
||||||
.padding(.vertical, 4)
|
.padding(.vertical, 4)
|
||||||
}
|
}
|
||||||
|
.navigationTitle("nav.operation_logs")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,15 +20,18 @@ struct VideoListView: View {
|
|||||||
private var totalPages: Int { max(1, (filteredVideos.count + pageSize - 1) / pageSize) }
|
private var totalPages: Int { max(1, (filteredVideos.count + pageSize - 1) / pageSize) }
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
if accountStore.activeAccount == nil {
|
Group {
|
||||||
ContentUnavailableView(
|
if accountStore.activeAccount == nil {
|
||||||
"error.no_active_account",
|
ContentUnavailableView {
|
||||||
systemImage: "person.slash",
|
Label("error.no_active_account", systemImage: "person.slash")
|
||||||
description: Text("account.add")
|
} description: {
|
||||||
)
|
Text("account.add")
|
||||||
} else {
|
}
|
||||||
content
|
} else {
|
||||||
|
content
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
.navigationTitle("nav.video_library")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
@@ -126,7 +129,8 @@ struct VideoListView: View {
|
|||||||
|
|
||||||
TableColumn("video.table.title") { video in
|
TableColumn("video.table.title") { video in
|
||||||
HStack {
|
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()
|
image.resizable().scaledToFill()
|
||||||
} placeholder: {
|
} placeholder: {
|
||||||
Color.gray
|
Color.gray
|
||||||
@@ -192,7 +196,7 @@ struct VideoListView: View {
|
|||||||
Text("\(pageSizes[i])").tag(i)
|
Text("\(pageSizes[i])").tag(i)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(width: 80)
|
.frame(width: 120)
|
||||||
.onChange(of: pageSizeIndex) { _, _ in
|
.onChange(of: pageSizeIndex) { _, _ in
|
||||||
pageNo = 1
|
pageNo = 1
|
||||||
updateDisplay()
|
updateDisplay()
|
||||||
|
|||||||
Reference in New Issue
Block a user