add live activity
This commit is contained in:
parent
013623290d
commit
f1864dc2ba
@ -7,6 +7,7 @@
|
||||
|
||||
import WidgetKit
|
||||
import SwiftUI
|
||||
import ActivityKit
|
||||
|
||||
private let appGroupId = "group.mobilemkch"
|
||||
|
||||
@ -178,3 +179,99 @@ struct FavoritesWidget: Widget {
|
||||
.description("Показывает избранные треды или топ по выбранной доске.")
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 16.1, *)
|
||||
struct ThreadActivityAttributes: ActivityAttributes {
|
||||
public struct ContentState: Codable, Hashable {
|
||||
var latestCommentText: String
|
||||
var commentsCount: Int
|
||||
var showTitle: Bool
|
||||
var showLastComment: Bool
|
||||
var showCommentCount: Bool
|
||||
var currentTitle: String
|
||||
var currentBoard: String
|
||||
}
|
||||
|
||||
var threadId: Int
|
||||
var title: String
|
||||
var board: String
|
||||
}
|
||||
|
||||
@available(iOS 16.1, *)
|
||||
struct ThreadLiveActivity: Widget {
|
||||
var body: some WidgetConfiguration {
|
||||
ActivityConfiguration(for: ThreadActivityAttributes.self) { context in
|
||||
ThreadLiveActivityView(context: context)
|
||||
} dynamicIsland: { context in
|
||||
DynamicIsland {
|
||||
DynamicIslandExpandedRegion(.leading) {
|
||||
Text("/\(context.state.currentBoard)/")
|
||||
.font(.caption)
|
||||
.foregroundColor(.blue)
|
||||
}
|
||||
DynamicIslandExpandedRegion(.center) {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
if context.state.showTitle {
|
||||
Text(context.state.currentTitle)
|
||||
.font(.footnote)
|
||||
.lineLimit(2)
|
||||
}
|
||||
if context.state.showLastComment && !context.state.latestCommentText.isEmpty {
|
||||
Text(context.state.latestCommentText)
|
||||
.font(.caption2)
|
||||
.lineLimit(2)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
DynamicIslandExpandedRegion(.trailing) {
|
||||
if context.state.showCommentCount {
|
||||
Label("\(context.state.commentsCount)", systemImage: "text.bubble")
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
} compactLeading: {
|
||||
Text("/\(context.state.currentBoard)/")
|
||||
.font(.caption2)
|
||||
} compactTrailing: {
|
||||
if context.state.showCommentCount {
|
||||
Text("\(context.state.commentsCount)")
|
||||
.font(.caption2)
|
||||
}
|
||||
} minimal: {
|
||||
Image(systemName: "text.bubble")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 16.1, *)
|
||||
private struct ThreadLiveActivityView: View {
|
||||
let context: ActivityViewContext<ThreadActivityAttributes>
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
HStack(spacing: 6) {
|
||||
Text("/\(context.state.currentBoard)/")
|
||||
.font(.caption)
|
||||
.foregroundColor(.blue)
|
||||
if context.state.showCommentCount {
|
||||
Label("\(context.state.commentsCount)", systemImage: "text.bubble")
|
||||
.font(.caption)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
if context.state.showTitle {
|
||||
Text(context.state.currentTitle)
|
||||
.font(.footnote)
|
||||
.lineLimit(2)
|
||||
}
|
||||
if context.state.showLastComment && !context.state.latestCommentText.isEmpty {
|
||||
Text(context.state.latestCommentText)
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
.lineLimit(2)
|
||||
}
|
||||
}
|
||||
.padding(12)
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,10 +7,14 @@
|
||||
|
||||
import WidgetKit
|
||||
import SwiftUI
|
||||
import ActivityKit
|
||||
|
||||
@main
|
||||
struct FavoritesWidgetBundle: WidgetBundle {
|
||||
var body: some Widget {
|
||||
FavoritesWidget()
|
||||
if #available(iOS 16.1, *) {
|
||||
ThreadLiveActivity()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,6 +6,8 @@
|
||||
<dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.widgetkit-extension</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>NSSupportsLiveActivities</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@ -6,6 +6,8 @@
|
||||
<array>
|
||||
<string>com.mkch.MobileMkch.backgroundrefresh</string>
|
||||
</array>
|
||||
<key>NSSupportsLiveActivities</key>
|
||||
<true/>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>background-processing</string>
|
||||
|
||||
201
MobileMkch/LiveActivityManager.swift
Normal file
201
MobileMkch/LiveActivityManager.swift
Normal file
@ -0,0 +1,201 @@
|
||||
import Foundation
|
||||
import ActivityKit
|
||||
|
||||
@available(iOS 16.1, *)
|
||||
final class LiveActivityManager {
|
||||
static let shared = LiveActivityManager()
|
||||
|
||||
private let storeKey = "activeThreadActivities"
|
||||
private var threadIdToActivityId: [Int: String] = [:]
|
||||
private var tickerTask: Task<Void, Never>?
|
||||
private var tickerActivityId: String?
|
||||
|
||||
private init() {
|
||||
load()
|
||||
}
|
||||
|
||||
func isActive(threadId: Int) -> Bool {
|
||||
return threadIdToActivityId[threadId] != nil && activity(for: threadId) != nil
|
||||
}
|
||||
|
||||
func start(for detail: ThreadDetail, comments: [Comment], settings: Settings) {
|
||||
let latestText = comments.last?.formattedText ?? ""
|
||||
let count = comments.count
|
||||
|
||||
let attributes = ThreadActivityAttributes(threadId: detail.id, title: detail.title, board: detail.board)
|
||||
let state = ThreadActivityAttributes.ContentState(
|
||||
latestCommentText: latestText,
|
||||
commentsCount: count,
|
||||
showTitle: settings.liveActivityShowTitle,
|
||||
showLastComment: settings.liveActivityShowLastComment,
|
||||
showCommentCount: settings.liveActivityShowCommentCount,
|
||||
currentTitle: detail.title,
|
||||
currentBoard: detail.board
|
||||
)
|
||||
|
||||
do {
|
||||
let activity = try Activity<ThreadActivityAttributes>.request(attributes: attributes, contentState: state, pushType: nil)
|
||||
threadIdToActivityId[detail.id] = activity.id
|
||||
save()
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
|
||||
func update(threadId: Int, comments: [Comment], settings: Settings) {
|
||||
guard let activity = activity(for: threadId) else { return }
|
||||
let latestText = comments.last?.formattedText ?? ""
|
||||
let count = comments.count
|
||||
let state = ThreadActivityAttributes.ContentState(
|
||||
latestCommentText: latestText,
|
||||
commentsCount: count,
|
||||
showTitle: settings.liveActivityShowTitle,
|
||||
showLastComment: settings.liveActivityShowLastComment,
|
||||
showCommentCount: settings.liveActivityShowCommentCount,
|
||||
currentTitle: "",
|
||||
currentBoard: ""
|
||||
)
|
||||
Task {
|
||||
await activity.update(using: state)
|
||||
}
|
||||
}
|
||||
|
||||
func end(threadId: Int) {
|
||||
guard let activity = activity(for: threadId) else { return }
|
||||
Task {
|
||||
await activity.end(dismissalPolicy: .immediate)
|
||||
}
|
||||
threadIdToActivityId.removeValue(forKey: threadId)
|
||||
save()
|
||||
}
|
||||
|
||||
private func activity(for threadId: Int) -> Activity<ThreadActivityAttributes>? {
|
||||
guard let id = threadIdToActivityId[threadId] else { return nil }
|
||||
return Activity<ThreadActivityAttributes>.activities.first(where: { $0.id == id })
|
||||
}
|
||||
|
||||
private func save() {
|
||||
if let data = try? JSONEncoder().encode(threadIdToActivityId) {
|
||||
UserDefaults.standard.set(data, forKey: storeKey)
|
||||
}
|
||||
}
|
||||
|
||||
private func load() {
|
||||
if let data = UserDefaults.standard.data(forKey: storeKey),
|
||||
let map = try? JSONDecoder().decode([Int: String].self, from: data) {
|
||||
threadIdToActivityId = map
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 16.1, *)
|
||||
struct ThreadActivityAttributes: ActivityAttributes {
|
||||
public struct ContentState: Codable, Hashable {
|
||||
var latestCommentText: String
|
||||
var commentsCount: Int
|
||||
var showTitle: Bool
|
||||
var showLastComment: Bool
|
||||
var showCommentCount: Bool
|
||||
var currentTitle: String
|
||||
var currentBoard: String
|
||||
}
|
||||
|
||||
var threadId: Int
|
||||
var title: String
|
||||
var board: String
|
||||
}
|
||||
|
||||
@available(iOS 16.1, *)
|
||||
extension LiveActivityManager {
|
||||
var isTickerRunning: Bool { tickerTask != nil }
|
||||
|
||||
func startTicker(settings: Settings, apiClient: APIClient) {
|
||||
stopTicker()
|
||||
let attributes = ThreadActivityAttributes(threadId: -1, title: "", board: "")
|
||||
let initial = ThreadActivityAttributes.ContentState(
|
||||
latestCommentText: "",
|
||||
commentsCount: 0,
|
||||
showTitle: settings.liveActivityShowTitle,
|
||||
showLastComment: settings.liveActivityShowLastComment,
|
||||
showCommentCount: settings.liveActivityShowCommentCount,
|
||||
currentTitle: "",
|
||||
currentBoard: ""
|
||||
)
|
||||
do {
|
||||
let activity = try Activity<ThreadActivityAttributes>.request(attributes: attributes, contentState: initial, pushType: nil)
|
||||
tickerActivityId = activity.id
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
tickerTask = Task { [weak self] in
|
||||
while !(Task.isCancelled) {
|
||||
guard let self = self, let activityId = self.tickerActivityId,
|
||||
let activity = Activity<ThreadActivityAttributes>.activities.first(where: { $0.id == activityId }) else { break }
|
||||
await withCheckedContinuation { (cont: CheckedContinuation<Void, Never>) in
|
||||
let boardHandler: (String) -> Void = { boardCode in
|
||||
apiClient.getThreads(forBoard: boardCode) { result in
|
||||
switch result {
|
||||
case .success(let threads):
|
||||
guard let thread = threads.randomElement() else {
|
||||
cont.resume()
|
||||
return
|
||||
}
|
||||
apiClient.getFullThread(boardCode: boardCode, threadId: thread.id) { full in
|
||||
switch full {
|
||||
case .success(let (detail, comments)):
|
||||
let text = comments.last?.formattedText ?? detail.text
|
||||
let count = comments.count
|
||||
let state = ThreadActivityAttributes.ContentState(
|
||||
latestCommentText: text,
|
||||
commentsCount: count,
|
||||
showTitle: settings.liveActivityShowTitle,
|
||||
showLastComment: settings.liveActivityShowLastComment,
|
||||
showCommentCount: settings.liveActivityShowCommentCount,
|
||||
currentTitle: detail.title,
|
||||
currentBoard: detail.board
|
||||
)
|
||||
Task { await activity.update(using: state) }
|
||||
cont.resume()
|
||||
case .failure:
|
||||
cont.resume()
|
||||
}
|
||||
}
|
||||
case .failure:
|
||||
cont.resume()
|
||||
}
|
||||
}
|
||||
}
|
||||
if settings.liveActivityTickerRandomBoard {
|
||||
apiClient.getBoards { boardsResult in
|
||||
switch boardsResult {
|
||||
case .success(let boards):
|
||||
if let random = boards.randomElement() {
|
||||
boardHandler(random.code)
|
||||
} else {
|
||||
cont.resume()
|
||||
}
|
||||
case .failure:
|
||||
cont.resume()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let code = settings.liveActivityTickerBoardCode.isEmpty ? settings.lastBoard : settings.liveActivityTickerBoardCode
|
||||
boardHandler(code)
|
||||
}
|
||||
}
|
||||
try? await Task.sleep(nanoseconds: UInt64(max(settings.liveActivityTickerInterval, 5)) * 1_000_000_000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func stopTicker() {
|
||||
tickerTask?.cancel()
|
||||
tickerTask = nil
|
||||
if let id = tickerActivityId,
|
||||
let activity = Activity<ThreadActivityAttributes>.activities.first(where: { $0.id == id }) {
|
||||
Task { await activity.end(dismissalPolicy: .immediate) }
|
||||
}
|
||||
tickerActivityId = nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -16,6 +16,14 @@ class Settings: ObservableObject {
|
||||
@Published var notificationInterval: Int = 300
|
||||
@Published var favoriteThreads: [FavoriteThread] = []
|
||||
@Published var offlineMode: Bool = false
|
||||
@Published var liveActivityEnabled: Bool = false
|
||||
@Published var liveActivityShowTitle: Bool = true
|
||||
@Published var liveActivityShowLastComment: Bool = true
|
||||
@Published var liveActivityShowCommentCount: Bool = true
|
||||
@Published var liveActivityTickerEnabled: Bool = false
|
||||
@Published var liveActivityTickerRandomBoard: Bool = true
|
||||
@Published var liveActivityTickerBoardCode: String = "b"
|
||||
@Published var liveActivityTickerInterval: Int = 15
|
||||
|
||||
private let userDefaults = UserDefaults.standard
|
||||
private let settingsKey = "MobileMkchSettings"
|
||||
@ -42,6 +50,14 @@ class Settings: ObservableObject {
|
||||
self.notificationInterval = settings.notificationInterval
|
||||
self.favoriteThreads = settings.favoriteThreads
|
||||
self.offlineMode = settings.offlineMode ?? false
|
||||
self.liveActivityEnabled = settings.liveActivityEnabled ?? false
|
||||
self.liveActivityShowTitle = settings.liveActivityShowTitle ?? true
|
||||
self.liveActivityShowLastComment = settings.liveActivityShowLastComment ?? true
|
||||
self.liveActivityShowCommentCount = settings.liveActivityShowCommentCount ?? true
|
||||
self.liveActivityTickerEnabled = settings.liveActivityTickerEnabled ?? false
|
||||
self.liveActivityTickerRandomBoard = settings.liveActivityTickerRandomBoard ?? true
|
||||
self.liveActivityTickerBoardCode = settings.liveActivityTickerBoardCode ?? "b"
|
||||
self.liveActivityTickerInterval = settings.liveActivityTickerInterval ?? 15
|
||||
}
|
||||
mirrorStateToAppGroup()
|
||||
}
|
||||
@ -63,6 +79,15 @@ class Settings: ObservableObject {
|
||||
favoriteThreads: favoriteThreads
|
||||
,
|
||||
offlineMode: offlineMode
|
||||
,
|
||||
liveActivityEnabled: liveActivityEnabled,
|
||||
liveActivityShowTitle: liveActivityShowTitle,
|
||||
liveActivityShowLastComment: liveActivityShowLastComment,
|
||||
liveActivityShowCommentCount: liveActivityShowCommentCount,
|
||||
liveActivityTickerEnabled: liveActivityTickerEnabled,
|
||||
liveActivityTickerRandomBoard: liveActivityTickerRandomBoard,
|
||||
liveActivityTickerBoardCode: liveActivityTickerBoardCode,
|
||||
liveActivityTickerInterval: liveActivityTickerInterval
|
||||
)
|
||||
|
||||
if let data = try? JSONEncoder().encode(settingsData) {
|
||||
@ -138,4 +163,12 @@ struct SettingsData: Codable {
|
||||
let notificationInterval: Int
|
||||
let favoriteThreads: [FavoriteThread]
|
||||
let offlineMode: Bool?
|
||||
let liveActivityEnabled: Bool?
|
||||
let liveActivityShowTitle: Bool?
|
||||
let liveActivityShowLastComment: Bool?
|
||||
let liveActivityShowCommentCount: Bool?
|
||||
let liveActivityTickerEnabled: Bool?
|
||||
let liveActivityTickerRandomBoard: Bool?
|
||||
let liveActivityTickerBoardCode: String?
|
||||
let liveActivityTickerInterval: Int?
|
||||
}
|
||||
@ -1,12 +1,14 @@
|
||||
import SwiftUI
|
||||
import Combine
|
||||
import Darwin
|
||||
import ActivityKit
|
||||
|
||||
struct SettingsView: View {
|
||||
@EnvironmentObject var settings: Settings
|
||||
@EnvironmentObject var apiClient: APIClient
|
||||
@EnvironmentObject var networkMonitor: NetworkMonitor
|
||||
|
||||
@State private var isTickerRunning = false
|
||||
@State private var showingAbout = false
|
||||
@State private var showingInfo = false
|
||||
@State private var testKeyResult: String?
|
||||
@ -203,6 +205,65 @@ struct SettingsView: View {
|
||||
}
|
||||
.padding(.trailing, 8)
|
||||
)
|
||||
if #available(iOS 16.1, *) {
|
||||
Toggle("Live Activity", isOn: $settings.liveActivityEnabled)
|
||||
.onReceive(Just(settings.liveActivityEnabled)) { _ in
|
||||
settings.saveSettings()
|
||||
}
|
||||
if settings.liveActivityEnabled {
|
||||
Toggle("Показывать заголовок", isOn: $settings.liveActivityShowTitle)
|
||||
.onReceive(Just(settings.liveActivityShowTitle)) { _ in settings.saveSettings() }
|
||||
Toggle("Показывать последний коммент", isOn: $settings.liveActivityShowLastComment)
|
||||
.onReceive(Just(settings.liveActivityShowLastComment)) { _ in settings.saveSettings() }
|
||||
Toggle("Показывать счётчик", isOn: $settings.liveActivityShowCommentCount)
|
||||
.onReceive(Just(settings.liveActivityShowCommentCount)) { _ in settings.saveSettings() }
|
||||
Toggle("Тикер случайных тредов", isOn: $settings.liveActivityTickerEnabled)
|
||||
.onReceive(Just(settings.liveActivityTickerEnabled)) { _ in settings.saveSettings() }
|
||||
if settings.liveActivityTickerEnabled {
|
||||
Toggle("Случайная борда", isOn: $settings.liveActivityTickerRandomBoard)
|
||||
.onReceive(Just(settings.liveActivityTickerRandomBoard)) { _ in settings.saveSettings() }
|
||||
if !settings.liveActivityTickerRandomBoard {
|
||||
HStack {
|
||||
Text("Код борды")
|
||||
TextField("b", text: $settings.liveActivityTickerBoardCode)
|
||||
.textInputAutocapitalization(.never)
|
||||
.disableAutocorrection(true)
|
||||
}
|
||||
.onReceive(Just(settings.liveActivityTickerBoardCode)) { _ in settings.saveSettings() }
|
||||
}
|
||||
HStack {
|
||||
Text("Интервал, сек")
|
||||
Spacer()
|
||||
Stepper(value: $settings.liveActivityTickerInterval, in: 5...120, step: 5) {
|
||||
Text("\(settings.liveActivityTickerInterval)")
|
||||
}
|
||||
}
|
||||
.onReceive(Just(settings.liveActivityTickerInterval)) { _ in settings.saveSettings() }
|
||||
HStack(spacing: 12) {
|
||||
Button("Старт тикера") {
|
||||
LiveActivityManager.shared.startTicker(settings: settings, apiClient: apiClient)
|
||||
isTickerRunning = true
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.tint(.green)
|
||||
Button("Стоп тикера") {
|
||||
LiveActivityManager.shared.stopTicker()
|
||||
isTickerRunning = false
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.tint(.red)
|
||||
Spacer()
|
||||
Text(isTickerRunning ? "Работает" : "Остановлен")
|
||||
.font(.caption)
|
||||
.foregroundColor(isTickerRunning ? .green : .secondary)
|
||||
}
|
||||
.onAppear { isTickerRunning = LiveActivityManager.shared.isTickerRunning }
|
||||
}
|
||||
Text("В фоне частые обновления ограничены системой")
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section("Управление кэшем") {
|
||||
@ -434,6 +495,7 @@ struct AboutView: View {
|
||||
struct DebugMenuView: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@EnvironmentObject var notificationManager: NotificationManager
|
||||
@State private var liveActivityStarted = false
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
@ -455,6 +517,25 @@ struct DebugMenuView: View {
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.foregroundColor(.blue)
|
||||
if #available(iOS 16.1, *) {
|
||||
Button(liveActivityStarted ? "Остановить Live Activity" : "Тест Live Activity") {
|
||||
if liveActivityStarted {
|
||||
LiveActivityManager.shared.end(threadId: 999999)
|
||||
liveActivityStarted = false
|
||||
} else {
|
||||
let detail = ThreadDetail(id: 999999, creation: "2023-01-01T00:00:00Z", title: "Тестовый тред", text: "", board: "b", files: [])
|
||||
let comments = [Comment(id: 1, text: "Привет из Live Activity", creation: "2023-01-01T00:00:00Z", files: [])]
|
||||
var s = Settings()
|
||||
s.liveActivityEnabled = true
|
||||
s.liveActivityShowTitle = true
|
||||
s.liveActivityShowLastComment = true
|
||||
s.liveActivityShowCommentCount = true
|
||||
LiveActivityManager.shared.start(for: detail, comments: comments, settings: s)
|
||||
liveActivityStarted = true
|
||||
}
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
@ -11,6 +11,7 @@ struct ThreadDetailView: View {
|
||||
@State private var isLoading = false
|
||||
@State private var errorMessage: String?
|
||||
@State private var showingAddComment = false
|
||||
@State private var activityOn = false
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
@ -88,8 +89,30 @@ struct ThreadDetailView: View {
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button("Обновить") {
|
||||
loadThreadDetail()
|
||||
HStack {
|
||||
Button("Обновить") { loadThreadDetail() }
|
||||
if #available(iOS 16.1, *) {
|
||||
Toggle("", isOn: $activityOn)
|
||||
.toggleStyle(SwitchToggleStyle(tint: .blue))
|
||||
.labelsHidden()
|
||||
.onChange(of: activityOn) { newValue in
|
||||
guard settings.liveActivityEnabled else { return }
|
||||
if newValue {
|
||||
if let detail = threadDetail {
|
||||
LiveActivityManager.shared.start(for: detail, comments: comments, settings: settings)
|
||||
}
|
||||
} else {
|
||||
LiveActivityManager.shared.end(threadId: thread.id)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if settings.liveActivityEnabled {
|
||||
if #available(iOS 16.1, *) {
|
||||
activityOn = LiveActivityManager.shared.isActive(threadId: thread.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -117,6 +140,9 @@ struct ThreadDetailView: View {
|
||||
case .success(let (detail, loadedComments)):
|
||||
self.threadDetail = detail
|
||||
self.comments = loadedComments
|
||||
if #available(iOS 16.1, *), settings.liveActivityEnabled, activityOn {
|
||||
LiveActivityManager.shared.update(threadId: thread.id, comments: loadedComments, settings: settings)
|
||||
}
|
||||
case .failure(let error):
|
||||
self.errorMessage = error.localizedDescription
|
||||
}
|
||||
|
||||
30
README.md
30
README.md
@ -77,6 +77,10 @@
|
||||
- **Debug меню** (5 тапов по информации об устройстве):
|
||||
- Тест краша приложения
|
||||
- Тестовые уведомления
|
||||
- **Live Activity (BETA)**:
|
||||
- Отображение треда на экране блокировки/Dynamic Island
|
||||
- Тикер случайных тредов по доскам с настраиваемым интервалом
|
||||
- Гибкие опции: заголовок, последний коммент, счётчик
|
||||
- **Управление кэшем**:
|
||||
- Очистка кэша досок
|
||||
- Очистка кэша тредов
|
||||
@ -110,6 +114,18 @@
|
||||
5. **Подпишитесь на нужные доски** переключателями
|
||||
6. **Протестируйте** кнопкой "Проверить новые треды сейчас"
|
||||
|
||||
### Live Activity (BETA)
|
||||
|
||||
1. Требования: iPhone c iOS 16.1+ (на iPad до iPadOS 18 Live Activity не показываются; на симуляторе поддержка ограничена)
|
||||
2. Включите: Настройки -> Уведомления -> Live Activity
|
||||
3. Отдельный тред: откройте тред и включите тумблер в правом верхнем углу
|
||||
4. Тикер:
|
||||
- В Настройки -> Уведомления включите "Тикер случайных тредов"
|
||||
- Выберите случайную борду или укажите код борды
|
||||
- Задайте интервал (5–120 сек)
|
||||
- Кнопки "Старт тикера" / "Стоп тикера"
|
||||
5. Ограничения платформы: бегущая строка в Live Activity недоступна. Контент обновляется дискретно через интервал. Частота обновлений в фоне ограничивается iOS.
|
||||
|
||||
### Оффлайн режим
|
||||
|
||||
1. Откройте вкладку "Настройки"
|
||||
@ -130,6 +146,7 @@
|
||||
| `APIClient.swift` | HTTP клиент с CSRF и аутентификацией |
|
||||
| `Settings.swift` | Система настроек с JSON сериализацией |
|
||||
| `Cache.swift` | Многоуровневое кэширование с TTL |
|
||||
| `LiveActivityManager.swift` | Управление Live Activity и тикером |
|
||||
|
||||
### UI компоненты
|
||||
|
||||
@ -144,6 +161,15 @@
|
||||
| `FileView.swift` | Просмотр файлов с полноэкранным режимом |
|
||||
| `NotificationSettingsView.swift` | BETA настройки уведомлений |
|
||||
|
||||
### Виджеты и Live Activity
|
||||
|
||||
| Файл | Описание |
|
||||
|------|----------|
|
||||
| `FavoritesWidget/FavoritesWidget.swift` | Конфигурация виджета и UI Live Activity |
|
||||
| `FavoritesWidget/FavoritesWidgetBundle.swift` | Регистрация виджета и Live Activity |
|
||||
| `FavoritesWidget/AppIntent.swift` | Intent-конфигурация виджета |
|
||||
| `FavoritesWidget/Info.plist` | Ключ `NSSupportsLiveActivities` |
|
||||
|
||||
### Системные сервисы
|
||||
|
||||
| Файл | Описание |
|
||||
@ -153,6 +179,7 @@
|
||||
| `CrashHandler.swift` | Обработка крашей |
|
||||
| `ImageLoader.swift` | Асинхронная загрузка изображений |
|
||||
| `NetworkMonitor.swift` | Монитор сети и принудительный оффлайн |
|
||||
| `AppGroup.swift` | Общие UserDefaults для app ↔ widget |
|
||||
|
||||
## API интеграция
|
||||
|
||||
@ -211,6 +238,7 @@ P.S. Костыль через Payload/MobileMkch.app в зипе и переи
|
||||
## Версии и обновления
|
||||
|
||||
### Версия 2.1.1-ios-alpha (Текущая)
|
||||
- Live Activity (BETA): тумблер в треде, тикер в "Настройки -> Уведомления"
|
||||
- Добавлен оффлайн режим (тумблер в настройках)
|
||||
- Дисковый кэш и фолбэк на сохранённые данные
|
||||
- Оффлайн-баннеры в списках и деталях
|
||||
@ -225,7 +253,7 @@ P.S. Костыль через Payload/MobileMkch.app в зипе и переи
|
||||
|
||||
p.s. я не уверен работает ли оно, но оно работает по крайней мере на моем устройстве
|
||||
|
||||
### Версия 2.0.0-ios-alpha (Текущая)
|
||||
### Версия 2.0.0-ios-alpha
|
||||
- Полная переработка UI на SwiftUI
|
||||
- Система избранного с локальным сохранением
|
||||
- Push-уведомления с фоновым обновлением
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user