2022-12-29 08:06:21 +01:00
|
|
|
//
|
|
|
|
// https://mczachurski.dev
|
2023-04-09 20:51:33 +02:00
|
|
|
// Copyright © 2023 Marcin Czachurski and the repository contributors.
|
2023-03-28 10:35:38 +02:00
|
|
|
// Licensed under the Apache License 2.0.
|
2022-12-29 08:06:21 +01:00
|
|
|
//
|
|
|
|
|
|
|
|
import SwiftUI
|
|
|
|
import UIKit
|
2023-10-20 07:45:18 +02:00
|
|
|
import SwiftData
|
2023-02-19 10:32:38 +01:00
|
|
|
import PixelfedKit
|
2023-04-07 14:20:12 +02:00
|
|
|
import ClientKit
|
2023-04-07 14:38:50 +02:00
|
|
|
import ServicesKit
|
2023-04-07 16:59:18 +02:00
|
|
|
import EnvironmentKit
|
2023-10-21 09:44:40 +02:00
|
|
|
import WidgetsKit
|
2022-12-29 08:06:21 +01:00
|
|
|
|
2023-10-19 13:24:02 +02:00
|
|
|
@MainActor
|
2023-04-01 12:10:59 +02:00
|
|
|
struct MainView: View {
|
2023-10-20 07:45:18 +02:00
|
|
|
@Environment(\.modelContext) private var modelContext
|
2023-02-03 15:16:30 +01:00
|
|
|
|
2023-10-19 13:24:02 +02:00
|
|
|
@Environment(ApplicationState.self) var applicationState
|
|
|
|
@Environment(Client.self) var client
|
|
|
|
@Environment(RouterPath.self) var routerPath
|
|
|
|
@Environment(TipsStore.self) var tipsStore
|
2023-04-01 12:10:59 +02:00
|
|
|
|
2023-04-20 15:03:43 +02:00
|
|
|
@State private var navBarTitle: LocalizedStringKey = ViewMode.home.title
|
2022-12-30 18:20:54 +01:00
|
|
|
@State private var viewMode: ViewMode = .home {
|
|
|
|
didSet {
|
2023-04-20 15:03:43 +02:00
|
|
|
self.navBarTitle = viewMode.title
|
2022-12-30 18:20:54 +01:00
|
|
|
}
|
|
|
|
}
|
2023-10-21 09:44:40 +02:00
|
|
|
|
|
|
|
private let mainNavigationTip = MainNavigationTip()
|
2023-10-20 07:45:18 +02:00
|
|
|
|
|
|
|
@Query(sort: \AccountData.acct, order: .forward) var dbAccounts: [AccountData]
|
2023-04-01 12:10:59 +02:00
|
|
|
|
2023-10-22 10:09:02 +02:00
|
|
|
public enum ViewMode: Int, Identifiable {
|
2023-04-21 16:58:52 +02:00
|
|
|
case home = 1
|
|
|
|
case local = 2
|
|
|
|
case federated = 3
|
|
|
|
case search = 4
|
|
|
|
case profile = 5
|
|
|
|
case notifications = 6
|
|
|
|
case trendingPhotos = 7
|
|
|
|
case trendingTags = 8
|
|
|
|
case trendingAccounts = 9
|
2023-04-20 15:03:43 +02:00
|
|
|
|
2023-10-22 10:09:02 +02:00
|
|
|
var id: Self {
|
|
|
|
return self
|
|
|
|
}
|
|
|
|
|
2023-04-20 15:03:43 +02:00
|
|
|
public var title: LocalizedStringKey {
|
|
|
|
switch self {
|
|
|
|
case .home:
|
|
|
|
return "mainview.tab.homeTimeline"
|
|
|
|
case .trendingPhotos:
|
|
|
|
return "mainview.tab.trendingPhotos"
|
|
|
|
case .trendingTags:
|
|
|
|
return "mainview.tab.trendingTags"
|
|
|
|
case .trendingAccounts:
|
|
|
|
return "mainview.tab.trendingAccounts"
|
|
|
|
case .local:
|
|
|
|
return "mainview.tab.localTimeline"
|
|
|
|
case .federated:
|
|
|
|
return "mainview.tab.federatedTimeline"
|
|
|
|
case .profile:
|
|
|
|
return "mainview.tab.userProfile"
|
|
|
|
case .notifications:
|
|
|
|
return "mainview.tab.notifications"
|
|
|
|
case .search:
|
|
|
|
return "mainview.tab.search"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-10-22 10:09:02 +02:00
|
|
|
@ViewBuilder
|
|
|
|
public func getImage(applicationState: ApplicationState) -> some View {
|
2023-04-20 15:03:43 +02:00
|
|
|
switch self {
|
|
|
|
case .home:
|
2023-10-22 10:09:02 +02:00
|
|
|
Image(systemName: "house")
|
2023-04-20 15:03:43 +02:00
|
|
|
case .trendingPhotos:
|
2023-10-22 10:09:02 +02:00
|
|
|
Image(systemName: "photo.stack")
|
2023-04-20 15:03:43 +02:00
|
|
|
case .trendingTags:
|
2023-10-22 10:09:02 +02:00
|
|
|
Image(systemName: "tag")
|
2023-04-20 15:03:43 +02:00
|
|
|
case .trendingAccounts:
|
2023-10-23 08:01:02 +02:00
|
|
|
Image(systemName: "person.crop.rectangle.stack")
|
2023-04-20 15:03:43 +02:00
|
|
|
case .local:
|
2023-10-22 10:09:02 +02:00
|
|
|
Image(systemName: "building")
|
2023-04-20 15:03:43 +02:00
|
|
|
case .federated:
|
2023-10-22 10:09:02 +02:00
|
|
|
Image(systemName: "globe.europe.africa")
|
2023-04-20 15:03:43 +02:00
|
|
|
case .profile:
|
2023-10-22 10:09:02 +02:00
|
|
|
Image(systemName: "person.crop.circle")
|
2023-04-20 15:03:43 +02:00
|
|
|
case .notifications:
|
2023-10-22 10:09:02 +02:00
|
|
|
if applicationState.menuPosition == .top {
|
2023-10-24 14:04:23 +02:00
|
|
|
applicationState.amountOfNewNotifications > 0 ? Image(systemName: "bell.badge") : Image(systemName: "bell")
|
2023-10-22 10:09:02 +02:00
|
|
|
} else {
|
2023-10-24 14:04:23 +02:00
|
|
|
applicationState.amountOfNewNotifications > 0
|
2023-10-22 10:09:02 +02:00
|
|
|
? AnyView(
|
|
|
|
Image(systemName: "bell.badge")
|
|
|
|
.symbolRenderingMode(.palette)
|
|
|
|
.foregroundStyle(applicationState.tintColor.color().opacity(0.75), Color.mainTextColor.opacity(0.75)))
|
|
|
|
: AnyView(Image(systemName: "bell"))
|
|
|
|
}
|
2023-04-20 15:03:43 +02:00
|
|
|
case .search:
|
2023-10-22 10:09:02 +02:00
|
|
|
Image(systemName: "magnifyingglass")
|
2023-04-20 15:03:43 +02:00
|
|
|
}
|
|
|
|
}
|
2022-12-30 18:20:54 +01:00
|
|
|
}
|
2023-04-01 12:10:59 +02:00
|
|
|
|
2022-12-29 08:06:21 +01:00
|
|
|
var body: some View {
|
2023-10-19 13:24:02 +02:00
|
|
|
@Bindable var applicationState = applicationState
|
|
|
|
@Bindable var routerPath = routerPath
|
2023-04-06 13:19:55 +02:00
|
|
|
|
2023-10-19 13:24:02 +02:00
|
|
|
NavigationStack(path: $routerPath.path) {
|
|
|
|
self.getMainView()
|
|
|
|
.navigationMenuButtons(menuPosition: $applicationState.menuPosition) { viewMode in
|
|
|
|
self.switchView(to: viewMode)
|
2023-04-06 13:19:55 +02:00
|
|
|
}
|
2023-10-19 13:24:02 +02:00
|
|
|
.navigationTitle(navBarTitle)
|
|
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
|
|
.toolbar {
|
|
|
|
self.getLeadingToolbar()
|
|
|
|
|
|
|
|
if self.applicationState.menuPosition == .top {
|
|
|
|
self.getPrincipalToolbar()
|
|
|
|
self.getTrailingToolbar()
|
2023-04-06 13:19:55 +02:00
|
|
|
}
|
2023-02-13 21:10:07 +01:00
|
|
|
}
|
2023-10-19 13:24:02 +02:00
|
|
|
.onChange(of: tipsStore.status) { oldStatus, newStatus in
|
|
|
|
if newStatus == .successful {
|
|
|
|
withAnimation(.spring()) {
|
|
|
|
self.routerPath.presentedOverlay = .successPayment
|
|
|
|
self.tipsStore.reset()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-12-30 18:20:54 +01:00
|
|
|
}
|
2023-04-01 12:10:59 +02:00
|
|
|
|
2022-12-30 18:20:54 +01:00
|
|
|
@ViewBuilder
|
|
|
|
private func getMainView() -> some View {
|
|
|
|
switch self.viewMode {
|
|
|
|
case .home:
|
2023-05-26 16:06:38 +02:00
|
|
|
if UIDevice.isIPhone {
|
2023-10-20 17:35:11 +02:00
|
|
|
HomeTimelineView()
|
2023-05-26 16:06:38 +02:00
|
|
|
.id(applicationState.account?.id ?? String.empty())
|
|
|
|
} else {
|
|
|
|
StatusesView(listType: .home)
|
|
|
|
.id(applicationState.account?.id ?? String.empty())
|
|
|
|
}
|
2023-03-04 14:08:39 +01:00
|
|
|
case .trendingPhotos:
|
2023-01-31 12:20:49 +01:00
|
|
|
TrendStatusesView(accountId: applicationState.account?.id ?? String.empty())
|
|
|
|
.id(applicationState.account?.id ?? String.empty())
|
2023-03-04 14:08:39 +01:00
|
|
|
case .trendingTags:
|
2023-03-07 16:45:44 +01:00
|
|
|
HashtagsView(listType: .trending)
|
2023-03-04 14:08:39 +01:00
|
|
|
.id(applicationState.account?.id ?? String.empty())
|
|
|
|
case .trendingAccounts:
|
2023-03-08 12:07:43 +01:00
|
|
|
AccountsPhotoView(listType: .trending)
|
2023-03-04 14:08:39 +01:00
|
|
|
.id(applicationState.account?.id ?? String.empty())
|
2022-12-30 18:20:54 +01:00
|
|
|
case .local:
|
2023-01-23 18:01:27 +01:00
|
|
|
StatusesView(listType: .local)
|
2023-01-31 12:20:49 +01:00
|
|
|
.id(applicationState.account?.id ?? String.empty())
|
2022-12-30 18:20:54 +01:00
|
|
|
case .federated:
|
2023-01-23 18:01:27 +01:00
|
|
|
StatusesView(listType: .federated)
|
2023-01-31 12:20:49 +01:00
|
|
|
.id(applicationState.account?.id ?? String.empty())
|
2023-01-05 21:08:19 +01:00
|
|
|
case .profile:
|
2023-01-31 12:20:49 +01:00
|
|
|
if let accountData = self.applicationState.account {
|
2023-01-05 21:08:19 +01:00
|
|
|
UserProfileView(accountId: accountData.id,
|
|
|
|
accountDisplayName: accountData.displayName,
|
2023-01-06 13:05:21 +01:00
|
|
|
accountUserName: accountData.acct)
|
2023-01-31 12:20:49 +01:00
|
|
|
.id(applicationState.account?.id ?? String.empty())
|
2023-01-05 21:08:19 +01:00
|
|
|
}
|
2022-12-30 18:20:54 +01:00
|
|
|
case .notifications:
|
2023-01-31 12:20:49 +01:00
|
|
|
if let accountData = self.applicationState.account {
|
2023-01-18 18:41:42 +01:00
|
|
|
NotificationsView(accountId: accountData.id)
|
2023-01-31 12:20:49 +01:00
|
|
|
.id(applicationState.account?.id ?? String.empty())
|
2023-01-18 18:41:42 +01:00
|
|
|
}
|
2023-03-06 16:00:53 +01:00
|
|
|
case .search:
|
|
|
|
SearchView()
|
|
|
|
.id(applicationState.account?.id ?? String.empty())
|
2022-12-30 18:20:54 +01:00
|
|
|
}
|
|
|
|
}
|
2023-04-01 12:10:59 +02:00
|
|
|
|
2022-12-30 18:20:54 +01:00
|
|
|
@ToolbarContentBuilder
|
|
|
|
private func getPrincipalToolbar() -> some ToolbarContent {
|
|
|
|
ToolbarItem(placement: .principal) {
|
|
|
|
Menu {
|
2023-04-21 16:58:52 +02:00
|
|
|
MainNavigationOptions(hiddenMenuItems: Binding.constant([])) { viewMode in
|
2023-04-20 15:03:43 +02:00
|
|
|
self.switchView(to: viewMode)
|
|
|
|
}
|
2022-12-30 18:20:54 +01:00
|
|
|
} label: {
|
|
|
|
HStack {
|
2023-03-13 13:53:36 +01:00
|
|
|
Text(navBarTitle, comment: "Navbar title")
|
2022-12-30 18:20:54 +01:00
|
|
|
.font(.headline)
|
|
|
|
Image(systemName: "chevron.down")
|
2023-02-19 10:16:01 +01:00
|
|
|
.fontWeight(.semibold)
|
2022-12-30 18:20:54 +01:00
|
|
|
.font(.subheadline)
|
|
|
|
}
|
|
|
|
.frame(width: 150)
|
2023-01-06 13:05:21 +01:00
|
|
|
.foregroundColor(.mainTextColor)
|
2023-10-21 09:44:40 +02:00
|
|
|
.popoverTip(self.mainNavigationTip)
|
2022-12-29 08:06:21 +01:00
|
|
|
}
|
|
|
|
}
|
2022-12-30 18:20:54 +01:00
|
|
|
}
|
2023-04-01 12:10:59 +02:00
|
|
|
|
2022-12-30 18:20:54 +01:00
|
|
|
@ToolbarContentBuilder
|
|
|
|
private func getLeadingToolbar() -> some ToolbarContent {
|
|
|
|
ToolbarItem(placement: .navigationBarLeading) {
|
2023-01-03 20:42:20 +01:00
|
|
|
Menu {
|
2023-01-11 13:16:43 +01:00
|
|
|
ForEach(self.dbAccounts) { account in
|
|
|
|
Button {
|
2023-03-08 12:07:43 +01:00
|
|
|
self.switchAccounts(account)
|
2023-01-11 13:16:43 +01:00
|
|
|
} label: {
|
2023-03-12 08:39:12 +01:00
|
|
|
HStack {
|
2023-01-11 13:16:43 +01:00
|
|
|
Text(account.displayName ?? account.acct)
|
2023-03-12 08:39:12 +01:00
|
|
|
self.getAvatarImage(avatarUrl: account.avatar, avatarData: account.avatarData)
|
2023-01-11 13:16:43 +01:00
|
|
|
}
|
2023-01-03 20:42:20 +01:00
|
|
|
}
|
2023-03-12 08:39:12 +01:00
|
|
|
.disabled(account.id == self.applicationState.account?.id)
|
2023-01-03 20:42:20 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
Divider()
|
2023-04-01 12:10:59 +02:00
|
|
|
|
2023-01-03 20:42:20 +01:00
|
|
|
Button {
|
2023-02-19 12:49:44 +01:00
|
|
|
HapticService.shared.fireHaptic(of: .buttonPress)
|
2023-01-23 18:01:27 +01:00
|
|
|
self.routerPath.presentedSheet = .settings
|
2023-01-03 20:42:20 +01:00
|
|
|
} label: {
|
2023-03-13 13:53:36 +01:00
|
|
|
Label("mainview.menu.settings", systemImage: "gear")
|
2023-01-03 20:42:20 +01:00
|
|
|
}
|
2022-12-30 18:20:54 +01:00
|
|
|
} label: {
|
2023-03-12 08:39:12 +01:00
|
|
|
self.getAvatarImage(avatarUrl: self.applicationState.account?.avatar,
|
|
|
|
avatarData: self.applicationState.account?.avatarData)
|
2022-12-29 08:06:21 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-04-01 12:10:59 +02:00
|
|
|
|
2023-01-12 18:34:48 +01:00
|
|
|
@ToolbarContentBuilder
|
|
|
|
private func getTrailingToolbar() -> some ToolbarContent {
|
2023-03-06 16:00:53 +01:00
|
|
|
if viewMode == .local || viewMode == .home || viewMode == .federated || viewMode == .trendingPhotos || viewMode == .search {
|
2023-01-23 09:10:58 +01:00
|
|
|
ToolbarItem(placement: .navigationBarTrailing) {
|
|
|
|
Button {
|
2023-02-19 12:49:44 +01:00
|
|
|
HapticService.shared.fireHaptic(of: .buttonPress)
|
2023-01-23 18:01:27 +01:00
|
|
|
self.routerPath.presentedSheet = .newStatusEditor
|
2023-01-23 09:10:58 +01:00
|
|
|
} label: {
|
2023-04-15 21:25:32 +02:00
|
|
|
Image(systemName: "plus")
|
2023-03-19 08:09:44 +01:00
|
|
|
.foregroundColor(Color.mainTextColor)
|
2023-02-19 10:16:01 +01:00
|
|
|
.fontWeight(.semibold)
|
2023-01-23 09:10:58 +01:00
|
|
|
}
|
2023-01-12 18:34:48 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-04-01 12:10:59 +02:00
|
|
|
|
2023-03-12 08:39:12 +01:00
|
|
|
@ViewBuilder
|
|
|
|
private func getAvatarImage(avatarUrl: URL?, avatarData: Data?) -> some View {
|
|
|
|
if let avatarData,
|
|
|
|
let uiImage = UIImage(data: avatarData)?.roundedAvatar(avatarShape: self.applicationState.avatarShape) {
|
|
|
|
Image(uiImage: uiImage)
|
|
|
|
.resizable()
|
|
|
|
.frame(width: 32.0, height: 32.0)
|
|
|
|
.clipShape(self.applicationState.avatarShape.shape())
|
|
|
|
} else if let avatarUrl {
|
|
|
|
AsyncImage(url: avatarUrl)
|
|
|
|
.frame(width: 32.0, height: 32.0)
|
|
|
|
.clipShape(self.applicationState.avatarShape.shape())
|
|
|
|
} else {
|
|
|
|
Image(systemName: "person")
|
|
|
|
.resizable()
|
|
|
|
.frame(width: 16, height: 16)
|
|
|
|
.foregroundColor(.white)
|
|
|
|
.padding(8)
|
2023-09-19 19:32:27 +02:00
|
|
|
.background(Color.customGrayColor)
|
2023-03-12 08:39:12 +01:00
|
|
|
.clipShape(AvatarShape.circle.shape())
|
|
|
|
.background(
|
|
|
|
AvatarShape.circle.shape()
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
2023-04-01 12:10:59 +02:00
|
|
|
|
2023-03-06 16:29:37 +01:00
|
|
|
private func switchView(to newViewMode: ViewMode) {
|
|
|
|
HapticService.shared.fireHaptic(of: .tabSelection)
|
2023-04-01 12:10:59 +02:00
|
|
|
|
2023-03-06 16:29:37 +01:00
|
|
|
if viewMode == .search {
|
2023-04-22 08:23:03 +02:00
|
|
|
self.hideKeyboard()
|
|
|
|
self.asyncAfter(0.3) {
|
2023-03-06 16:29:37 +01:00
|
|
|
self.viewMode = newViewMode
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
self.viewMode = newViewMode
|
|
|
|
}
|
|
|
|
}
|
2023-04-01 12:10:59 +02:00
|
|
|
|
2023-03-08 12:07:43 +01:00
|
|
|
private func switchAccounts(_ account: AccountData) {
|
|
|
|
HapticService.shared.fireHaptic(of: .buttonPress)
|
2023-04-01 12:10:59 +02:00
|
|
|
|
2023-03-08 12:07:43 +01:00
|
|
|
if viewMode == .search {
|
2023-04-22 08:23:03 +02:00
|
|
|
self.hideKeyboard()
|
|
|
|
self.asyncAfter(0.3) {
|
2023-03-08 12:07:43 +01:00
|
|
|
self.tryToSwitch(account)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
self.tryToSwitch(account)
|
|
|
|
}
|
|
|
|
}
|
2023-04-01 12:10:59 +02:00
|
|
|
|
2023-02-12 09:13:04 +01:00
|
|
|
private func tryToSwitch(_ account: AccountData) {
|
|
|
|
Task {
|
|
|
|
// Verify access token correctness.
|
|
|
|
let authorizationSession = AuthorizationSession()
|
2023-04-07 14:20:12 +02:00
|
|
|
let accountModel = account.toAccountModel()
|
2023-04-01 12:10:59 +02:00
|
|
|
|
2023-10-20 07:45:18 +02:00
|
|
|
await AuthorizationService.shared.verifyAccount(session: authorizationSession,
|
|
|
|
accountModel: accountModel,
|
|
|
|
modelContext: modelContext) { signedInAccountModel in
|
2023-03-29 17:06:41 +02:00
|
|
|
guard let signedInAccountModel else {
|
2023-10-20 17:35:11 +02:00
|
|
|
ToastrService.shared.showError(title: "", subtitle: NSLocalizedString("mainview.error.switchAccounts", comment: "Cannot switch accounts."))
|
2023-02-12 09:13:04 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
Task { @MainActor in
|
2023-03-29 17:06:41 +02:00
|
|
|
let instance = try? await self.client.instances.instance(url: signedInAccountModel.serverUrl)
|
2023-02-22 13:26:35 +01:00
|
|
|
|
|
|
|
// Refresh client state.
|
2023-03-29 17:06:41 +02:00
|
|
|
self.client.setAccount(account: signedInAccountModel)
|
2023-04-01 12:10:59 +02:00
|
|
|
|
2023-02-22 13:26:35 +01:00
|
|
|
// Refresh application state.
|
2023-03-29 17:06:41 +02:00
|
|
|
self.applicationState.changeApplicationState(accountModel: signedInAccountModel,
|
2023-02-22 13:26:35 +01:00
|
|
|
instance: instance,
|
2023-10-22 10:09:02 +02:00
|
|
|
lastSeenStatusId: signedInAccountModel.lastSeenStatusId,
|
|
|
|
lastSeenNotificationId: signedInAccountModel.lastSeenNotificationId)
|
2023-02-22 13:26:35 +01:00
|
|
|
|
|
|
|
// Set account as default (application will open this account after restart).
|
2023-10-20 07:45:18 +02:00
|
|
|
ApplicationSettingsHandler.shared.set(accountId: signedInAccountModel.id, modelContext: modelContext)
|
2023-10-22 14:20:32 +02:00
|
|
|
|
|
|
|
// Refresh new photos and notifications.
|
|
|
|
_ = await (self.calculateNewPhotosInBackground(), self.calculateNewNotificationsInBackground())
|
2023-02-12 09:13:04 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-10-22 14:20:32 +02:00
|
|
|
|
|
|
|
private func calculateNewPhotosInBackground() async {
|
|
|
|
if let account = self.applicationState.account {
|
|
|
|
self.applicationState.amountOfNewStatuses = await HomeTimelineService.shared.amountOfNewStatuses(
|
|
|
|
for: account,
|
|
|
|
includeReblogs: self.applicationState.showReboostedStatuses,
|
|
|
|
hideStatusesWithoutAlt: self.applicationState.hideStatusesWithoutAlt,
|
|
|
|
modelContext: modelContext
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func calculateNewNotificationsInBackground() async {
|
|
|
|
if let account = self.applicationState.account {
|
2023-10-24 14:04:23 +02:00
|
|
|
self.applicationState.amountOfNewNotifications = await NotificationsService.shared.amountOfNewNotifications(for: account, modelContext: modelContext)
|
2023-10-22 14:20:32 +02:00
|
|
|
}
|
|
|
|
}
|
2022-12-29 08:06:21 +01:00
|
|
|
}
|