feat: 多模块功能更新 - 文件传输/多语言/NFC/首页组件/进度美化等
- 文件传输: 设备发现、LAN发现服务优化 - NFC分享: provider和service增强 - 多语言: 16种语言翻译补全 - 首页: 句子详情面板、收藏页、离线页优化 - 我的: 成就、个人资料、签到、设置页面更新 - 新增: AR视图、进度美化页、Hive安全访问、鸿蒙兼容助手、共享组件 - iOS Widget: Intents扩展、XianyanWidget更新 - 鸿蒙: 6个卡片页面更新 - 其他: 路由注册、缓存配置、崩溃监控、TTS播放器等
This commit is contained in:
131
ios/XianyanWidget/Intents/XianyanWidgetIntents.swift
Normal file
131
ios/XianyanWidget/Intents/XianyanWidgetIntents.swift
Normal file
@@ -0,0 +1,131 @@
|
||||
// ============================================================
|
||||
// 闲言Widget — AppIntent 定义
|
||||
// 创建时间: 2026-06-04
|
||||
// 更新时间: 2026-06-04
|
||||
// 作用: 定义iOS Widget交互式操作意图(点赞/分享/切换/刷新)
|
||||
// 要求: iOS 17.0+ 完整支持,iOS 14-16 降级为Link方式
|
||||
// 注意: Widget Extension 中不可使用 UIApplication.shared,
|
||||
// 通过 App Group UserDefaults 与主APP通信
|
||||
// ============================================================
|
||||
|
||||
import WidgetKit
|
||||
import AppIntents
|
||||
|
||||
private let sharedDefaults = UserDefaults(suiteName: "group.apps.xy.xianyan.share")
|
||||
|
||||
// MARK: - 刷新Widget Intent
|
||||
|
||||
/// 刷新Widget内容
|
||||
struct RefreshWidgetIntent: AppIntent {
|
||||
static var title: LocalizedStringResource = "刷新"
|
||||
static var description: IntentDescription = "刷新当前Widget内容"
|
||||
|
||||
func perform() async throws -> some IntentResult {
|
||||
// 记录待刷新的widget类型,主APP启动时读取处理
|
||||
sharedDefaults?.set("DailySentence", forKey: "widget_pending_refresh")
|
||||
sharedDefaults?.synchronize()
|
||||
// 请求WidgetKit刷新timeline
|
||||
WidgetCenter.shared.reloadTimelines(ofKind: "DailySentenceWidget")
|
||||
return .result()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 点赞句子 Intent
|
||||
|
||||
/// 点赞当前句子
|
||||
struct LikeSentenceIntent: AppIntent {
|
||||
static var title: LocalizedStringResource = "点赞"
|
||||
static var description: IntentDescription = "喜欢这句句子"
|
||||
|
||||
func perform() async throws -> some IntentResult {
|
||||
// 通过UserDefaults通知主APP执行点赞
|
||||
sharedDefaults?.set(true, forKey: "widget_action_like")
|
||||
sharedDefaults?.set(Date(), forKey: "widget_action_like_time")
|
||||
sharedDefaults?.synchronize()
|
||||
return .result()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 分享内容 Intent
|
||||
|
||||
/// 分享当前内容
|
||||
struct ShareContentIntent: AppIntent {
|
||||
static var title: LocalizedStringResource = "分享"
|
||||
static var description: IntentDescription = "分享当前内容"
|
||||
|
||||
func perform() async throws -> some IntentResult {
|
||||
// 记录分享请求,主APP处理实际分享逻辑(默认分享句子)
|
||||
sharedDefaults?.set("sentence", forKey: "widget_action_share_type")
|
||||
sharedDefaults?.set(Date(), forKey: "widget_action_share_time")
|
||||
sharedDefaults?.synchronize()
|
||||
return .result()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 切换下一条内容 Intent
|
||||
|
||||
/// 切换到下一条内容
|
||||
struct NextContentIntent: AppIntent {
|
||||
static var title: LocalizedStringResource = "下一句"
|
||||
static var description: IntentDescription = "切换显示下一条内容"
|
||||
|
||||
func perform() async throws -> some IntentResult {
|
||||
// 记录切换请求
|
||||
sharedDefaults?.set("DailySentence", forKey: "widget_action_next_widget")
|
||||
sharedDefaults?.set(Date(), forKey: "widget_action_next_time")
|
||||
sharedDefaults?.synchronize()
|
||||
// 刷新对应widget
|
||||
WidgetCenter.shared.reloadTimelines(ofKind: "DailySentenceWidget")
|
||||
return .result()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 签到 Intent
|
||||
|
||||
/// 执行每日签到
|
||||
struct CheckinIntent: AppIntent {
|
||||
static var title: LocalizedStringResource = "签到"
|
||||
static var description: IntentDescription = "执行今日签到"
|
||||
|
||||
func perform() async throws -> some IntentResult {
|
||||
// 标记签到请求,由主APP完成实际签到并回写结果
|
||||
sharedDefaults?.set(true, forKey: "widget_action_checkin")
|
||||
sharedDefaults?.set(Date(), forKey: "widget_action_checkin_time")
|
||||
sharedDefaults?.synchronize()
|
||||
// 刷新签到widget
|
||||
WidgetCenter.shared.reloadTimelines(ofKind: "CheckinWidget")
|
||||
return .result()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 打开APP特定页面 Intent
|
||||
|
||||
/// 打开APP并跳转到指定页面
|
||||
struct OpenAppPageIntent: AppIntent {
|
||||
static var title: LocalizedStringResource = "打开APP"
|
||||
static var description: IntentDescription = "打开闲言APP"
|
||||
|
||||
func perform() async throws -> some IntentResult {
|
||||
// 记录目标页面,主APP启动时读取跳转
|
||||
sharedDefaults?.set("/home", forKey: "widget_open_page")
|
||||
sharedDefaults?.set(Date(), forKey: "widget_open_page_time")
|
||||
sharedDefaults?.synchronize()
|
||||
return .result()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 保存日签卡片 Intent
|
||||
|
||||
/// 保存日签卡片图片
|
||||
struct SaveCardIntent: AppIntent {
|
||||
static var title: LocalizedStringResource = "保存"
|
||||
static var description: IntentDescription = "保存日签卡片到相册"
|
||||
|
||||
func perform() async throws -> some IntentResult {
|
||||
// 记录保存请求,主APP执行实际的图片生成和保存
|
||||
sharedDefaults?.set(true, forKey: "widget_action_save_card")
|
||||
sharedDefaults?.set(Date(), forKey: "widget_action_save_card_time")
|
||||
sharedDefaults?.synchronize()
|
||||
return .result()
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import WidgetKit
|
||||
import SwiftUI
|
||||
import AppIntents
|
||||
|
||||
struct DailySentenceEntry: TimelineEntry {
|
||||
let date: Date
|
||||
@@ -322,11 +323,59 @@ struct DailySentenceWidgetEntryView: View {
|
||||
var body: some View {
|
||||
let colors = WidgetColors(isDark: entry.isDark)
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text(entry.sentence)
|
||||
.font(.body)
|
||||
.foregroundColor(colors.primary)
|
||||
.lineLimit(3)
|
||||
.multilineTextAlignment(.leading)
|
||||
HStack {
|
||||
Text(entry.sentence)
|
||||
.font(.body)
|
||||
.foregroundColor(colors.primary)
|
||||
.lineLimit(3)
|
||||
.multilineTextAlignment(.leading)
|
||||
Spacer()
|
||||
// 交互式按钮区域(iOS 17+)
|
||||
if #available(iOS 17.0, *) {
|
||||
HStack(spacing: 6) {
|
||||
// 点赞按钮
|
||||
Button(intent: LikeSentenceIntent()) {
|
||||
Image(systemName: "heart")
|
||||
.font(.caption2)
|
||||
.foregroundColor(colors.secondary)
|
||||
.padding(4)
|
||||
.background(colors.bg.opacity(0.5))
|
||||
.clipShape(Circle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
// 分享按钮
|
||||
Button(intent: ShareContentIntent()) {
|
||||
Image(systemName: "square.and.arrow.up")
|
||||
.font(.caption2)
|
||||
.foregroundColor(colors.secondary)
|
||||
.padding(4)
|
||||
.background(colors.bg.opacity(0.5))
|
||||
.clipShape(Circle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
// 刷新按钮
|
||||
Button(intent: RefreshWidgetIntent()) {
|
||||
Image(systemName: "arrow.clockwise")
|
||||
.font(.caption2)
|
||||
.foregroundColor(colors.secondary)
|
||||
.padding(4)
|
||||
.background(colors.bg.opacity(0.5))
|
||||
.clipShape(Circle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
} else {
|
||||
// iOS 14-16 降级:仅刷新链接
|
||||
Link(destination: URL(string: "xianyanwidget://refresh?widget=DailySentence")!) {
|
||||
Image(systemName: "arrow.clockwise")
|
||||
.font(.caption2)
|
||||
.foregroundColor(colors.secondary)
|
||||
.padding(4)
|
||||
.background(colors.bg.opacity(0.5))
|
||||
.clipShape(Circle())
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
Text("— \(entry.author)")
|
||||
.font(.caption)
|
||||
@@ -348,6 +397,40 @@ struct ReadlaterWidgetEntryView: View {
|
||||
.font(.headline)
|
||||
.foregroundColor(colors.primary)
|
||||
Spacer()
|
||||
// 交互式按钮区域(iOS 17+)
|
||||
if #available(iOS 17.0, *) {
|
||||
HStack(spacing: 6) {
|
||||
// 打开阅读按钮
|
||||
Button(intent: OpenAppPageIntent()) {
|
||||
Image(systemName: "book.fill")
|
||||
.font(.caption2)
|
||||
.foregroundColor(colors.secondary)
|
||||
.padding(4)
|
||||
.background(colors.bg.opacity(0.5))
|
||||
.clipShape(Circle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
// 刷新按钮
|
||||
Button(intent: RefreshWidgetIntent()) {
|
||||
Image(systemName: "arrow.clockwise")
|
||||
.font(.caption2)
|
||||
.foregroundColor(colors.secondary)
|
||||
.padding(4)
|
||||
.background(colors.bg.opacity(0.5))
|
||||
.clipShape(Circle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
} else {
|
||||
Link(destination: URL(string: "xianyanwidget://refresh?widget=Readlater")!) {
|
||||
Image(systemName: "arrow.clockwise")
|
||||
.font(.caption2)
|
||||
.foregroundColor(colors.secondary)
|
||||
.padding(4)
|
||||
.background(colors.bg.opacity(0.5))
|
||||
.clipShape(Circle())
|
||||
}
|
||||
}
|
||||
}
|
||||
if !entry.previewText.isEmpty {
|
||||
Text(entry.previewText)
|
||||
@@ -366,6 +449,53 @@ struct DailyCardWidgetEntryView: View {
|
||||
var body: some View {
|
||||
let colors = WidgetColors(isDark: entry.isDark)
|
||||
VStack(spacing: 8) {
|
||||
HStack {
|
||||
Spacer()
|
||||
// 交互式按钮区域(iOS 17+)
|
||||
if #available(iOS 17.0, *) {
|
||||
HStack(spacing: 6) {
|
||||
// 保存按钮
|
||||
Button(intent: SaveCardIntent()) {
|
||||
Image(systemName: "square.and.arrow.down")
|
||||
.font(.caption2)
|
||||
.foregroundColor(colors.secondary)
|
||||
.padding(4)
|
||||
.background(colors.bg.opacity(0.5))
|
||||
.clipShape(Circle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
// 分享按钮
|
||||
Button(intent: ShareContentIntent()) {
|
||||
Image(systemName: "square.and.arrow.up")
|
||||
.font(.caption2)
|
||||
.foregroundColor(colors.secondary)
|
||||
.padding(4)
|
||||
.background(colors.bg.opacity(0.5))
|
||||
.clipShape(Circle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
// 刷新按钮
|
||||
Button(intent: RefreshWidgetIntent()) {
|
||||
Image(systemName: "arrow.clockwise")
|
||||
.font(.caption2)
|
||||
.foregroundColor(colors.secondary)
|
||||
.padding(4)
|
||||
.background(colors.bg.opacity(0.5))
|
||||
.clipShape(Circle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
} else {
|
||||
Link(destination: URL(string: "xianyanwidget://refresh?widget=DailyCard")!) {
|
||||
Image(systemName: "arrow.clockwise")
|
||||
.font(.caption2)
|
||||
.foregroundColor(colors.secondary)
|
||||
.padding(4)
|
||||
.background(colors.bg.opacity(0.5))
|
||||
.clipShape(Circle())
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
Text(entry.sentence)
|
||||
.font(.body)
|
||||
@@ -387,8 +517,55 @@ struct FortuneWidgetEntryView: View {
|
||||
var body: some View {
|
||||
let colors = WidgetColors(isDark: entry.isDark)
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text(entry.keyword)
|
||||
.font(.title2)
|
||||
HStack {
|
||||
Text(entry.keyword)
|
||||
.font(.title2)
|
||||
Spacer()
|
||||
// 交互式按钮区域(iOS 17+)
|
||||
if #available(iOS 17.0, *) {
|
||||
HStack(spacing: 6) {
|
||||
// 换一条按钮
|
||||
Button(intent: NextContentIntent()) {
|
||||
Image(systemName: "shuffle")
|
||||
.font(.caption2)
|
||||
.foregroundColor(colors.secondary)
|
||||
.padding(4)
|
||||
.background(colors.bg.opacity(0.5))
|
||||
.clipShape(Circle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
// 分享按钮
|
||||
Button(intent: ShareContentIntent()) {
|
||||
Image(systemName: "square.and.arrow.up")
|
||||
.font(.caption2)
|
||||
.foregroundColor(colors.secondary)
|
||||
.padding(4)
|
||||
.background(colors.bg.opacity(0.5))
|
||||
.clipShape(Circle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
// 刷新按钮
|
||||
Button(intent: RefreshWidgetIntent()) {
|
||||
Image(systemName: "arrow.clockwise")
|
||||
.font(.caption2)
|
||||
.foregroundColor(colors.secondary)
|
||||
.padding(4)
|
||||
.background(colors.bg.opacity(0.5))
|
||||
.clipShape(Circle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
} else {
|
||||
Link(destination: URL(string: "xianyanwidget://refresh?widget=Fortune")!) {
|
||||
Image(systemName: "arrow.clockwise")
|
||||
.font(.caption2)
|
||||
.foregroundColor(colors.secondary)
|
||||
.padding(4)
|
||||
.background(colors.bg.opacity(0.5))
|
||||
.clipShape(Circle())
|
||||
}
|
||||
}
|
||||
}
|
||||
Text(entry.text)
|
||||
.font(.caption)
|
||||
.foregroundColor(colors.primary)
|
||||
@@ -443,10 +620,21 @@ struct SolarTermWidgetEntryView: View {
|
||||
var body: some View {
|
||||
let colors = WidgetColors(isDark: entry.isDark)
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text(entry.name)
|
||||
.font(.title3)
|
||||
.fontWeight(.bold)
|
||||
.foregroundColor(colors.primary)
|
||||
HStack {
|
||||
Text(entry.name)
|
||||
.font(.title3)
|
||||
.fontWeight(.bold)
|
||||
.foregroundColor(colors.primary)
|
||||
Spacer()
|
||||
Link(destination: URL(string: "xianyanwidget://refresh?widget=SolarTerm")!) {
|
||||
Image(systemName: "arrow.clockwise")
|
||||
.font(.caption2)
|
||||
.foregroundColor(colors.secondary)
|
||||
.padding(4)
|
||||
.background(colors.bg.opacity(0.5))
|
||||
.clipShape(Circle())
|
||||
}
|
||||
}
|
||||
Text(entry.poem)
|
||||
.font(.caption)
|
||||
.foregroundColor(colors.secondary)
|
||||
@@ -462,10 +650,45 @@ struct CheckinWidgetEntryView: View {
|
||||
var body: some View {
|
||||
let colors = WidgetColors(isDark: entry.isDark)
|
||||
VStack(spacing: 4) {
|
||||
Text("连续\(entry.days)天")
|
||||
.font(.title3)
|
||||
.fontWeight(.bold)
|
||||
.foregroundColor(colors.primary)
|
||||
HStack {
|
||||
Text("连续\(entry.days)天")
|
||||
.font(.title3)
|
||||
.fontWeight(.bold)
|
||||
.foregroundColor(colors.primary)
|
||||
Spacer()
|
||||
// 交互式按钮区域(iOS 17+)
|
||||
if #available(iOS 17.0, *) {
|
||||
// 签到按钮
|
||||
Button(intent: CheckinIntent()) {
|
||||
Text(entry.todayDone ? "✅" : "📝")
|
||||
.font(.caption2)
|
||||
.foregroundColor(colors.secondary)
|
||||
.padding(6)
|
||||
.background(colors.bg.opacity(0.5))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
// 刷新按钮
|
||||
Button(intent: RefreshWidgetIntent()) {
|
||||
Image(systemName: "arrow.clockwise")
|
||||
.font(.caption2)
|
||||
.foregroundColor(colors.secondary)
|
||||
.padding(4)
|
||||
.background(colors.bg.opacity(0.5))
|
||||
.clipShape(Circle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
} else {
|
||||
Link(destination: URL(string: "xianyanwidget://refresh?widget=Checkin")!) {
|
||||
Image(systemName: "arrow.clockwise")
|
||||
.font(.caption2)
|
||||
.foregroundColor(colors.secondary)
|
||||
.padding(4)
|
||||
.background(colors.bg.opacity(0.5))
|
||||
.clipShape(Circle())
|
||||
}
|
||||
}
|
||||
}
|
||||
Text(entry.todayDone ? "✅ 今日已签" : "📝 点击签到")
|
||||
.font(.caption)
|
||||
.foregroundColor(colors.secondary)
|
||||
|
||||
Reference in New Issue
Block a user