diff --git a/.github/workflows/crowdin-upload.yml b/.github/workflows/crowdin-upload.yml index 9385fa127a..129d971dfa 100644 --- a/.github/workflows/crowdin-upload.yml +++ b/.github/workflows/crowdin-upload.yml @@ -5,7 +5,7 @@ on: branches: [master] paths: - "**/values/strings.xml" - - "iosApp/flare/Localizable.xcstrings" + - "appleApp/ios/Localizable.xcstrings" jobs: crowdin: diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml index 7f1663f4d1..a5c75841a9 100644 --- a/.github/workflows/ios.yml +++ b/.github/workflows/ios.yml @@ -44,11 +44,11 @@ jobs: - name: Install XcodeGen run: brew install xcodegen - name: Generate Xcode project - run: xcodegen generate --spec iosApp/project.yml + run: xcodegen generate --spec appleApp/project.yml - name: Build - working-directory: ./iosApp + working-directory: ./appleApp env: - scheme: Flare + scheme: iOS platform: ${{ 'iOS Simulator' }} run: | # xcrun xctrace returns via stderr, not the expected stdout (see https://developer.apple.com/forums/thread/663959) diff --git a/.gitignore b/.gitignore index 7be4b923ac..8090a4fb91 100644 --- a/.gitignore +++ b/.gitignore @@ -18,15 +18,15 @@ local.properties signing.properties build/ -iosApp/Podfile.lock -iosApp/Pods/* -iosApp/Flare.xcworkspace/* -iosApp/Flare.xcodeproj/* +*/Podfile.lock +*/Pods/* +*/Flare.xcworkspace/* +*/Flare.xcodeproj/* shared/shared.podspec .kotlin .history .lh -iosApp/K_D/ +appleApp/K_D/ *.sh !.github/scripts/*.sh .env diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 490283815e..2a44570e72 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -34,7 +34,7 @@ Flare uses [ktlint](https://github.com/pinterest/ktlint) to check the code style ### iOS - Make sure you have JDK 25 installed - Make sure you have a Mac with Xcode 26 installed - - open `iosApp/Flare.xcodeproj` in Xcode + - open `appleApp/Flare.xcodeproj` in Xcode - Build and run the app ### Desktop @@ -46,10 +46,10 @@ The project is split into theses parts: - `shared`: The common code, including bussiness logic. - `compose-ui`: The Compose UI code that shared between Android, iOS, Desktop. - `app`: The Android app. - - `iosApp`: The iOS app. + - `appleApp`: The iOS and macOS apps. - `desktopApp`: The desktop app for Windows/macOS. -Most of the business logic is in `shared`, and the platform specific code and UI is in `app` and `iosApp`. +Most of the business logic is in `shared`, and the platform specific code and UI is in `app` and `appleApp`. Flare uses [Kotlin Multiplatform](https://kotlinlang.org/docs/multiplatform.html) to share code between platforms, [Jetpack Compose](https://developer.android.com/jetpack/compose) for the UI on Android, [SwiftUI](https://developer.apple.com/xcode/swiftui/) for the UI on iOS. ### Business logic diff --git a/README.md b/README.md index 5a69af0db7..a6da95dd6e 100644 --- a/README.md +++ b/README.md @@ -56,8 +56,8 @@ Here're some features we're planning to implement in the future. - Make sure you have JDK 25 installed - Make sure you have a Mac with Xcode 26 installed - Install XcodeGen with `brew install xcodegen` - - Run `xcodegen generate --spec iosApp/project.yml` - - open `iosApp/Flare.xcodeproj` in Xcode + - Run `xcodegen generate --spec appleApp/project.yml` + - open `appleApp/Flare.xcodeproj` in Xcode - Build and run the app ### Desktop diff --git a/ios-shared/build.gradle.kts b/apple-shared/build.gradle.kts similarity index 63% rename from ios-shared/build.gradle.kts rename to apple-shared/build.gradle.kts index c02e3a55a5..9eb5a2f731 100644 --- a/ios-shared/build.gradle.kts +++ b/apple-shared/build.gradle.kts @@ -1,6 +1,6 @@ +import co.touchlab.skie.configuration.DefaultArgumentInterop import dev.dimension.flare.buildlogic.FlarePlatform import dev.dimension.flare.buildlogic.flare -import co.touchlab.skie.configuration.DefaultArgumentInterop import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget plugins { @@ -13,31 +13,47 @@ plugins { kotlin { flare { - namespace = "dev.dimension.flare.ios.shared" + namespace = "dev.dimension.flare.apple.shared" platforms( FlarePlatform.IOS, + FlarePlatform.MACOS, ) } - listOf("iosArm64", "iosSimulatorArm64") + val commonExportedProjects = + listOf( + projects.shared, + projects.social.bluesky, + projects.social.mastodon, + projects.social.misskey, + projects.social.pixiv, + projects.social.vvo, + projects.social.xqt, + projects.feature.loginApi, + projects.feature.login, + projects.feature.subscription, + projects.feature.tab, + ) + + listOf("iosArm64", "iosSimulatorArm64", "macosArm64") .map { targetName -> targets.getByName(targetName) as KotlinNativeTarget } .forEach { appleTarget -> appleTarget.binaries.framework { baseName = "KotlinSharedUI" isStatic = true - export(projects.shared) - export(projects.social.bluesky) - export(projects.social.mastodon) - export(projects.social.misskey) - export(projects.social.nostr) - export(projects.social.pixiv) - export(projects.social.vvo) - export(projects.social.xqt) - export(projects.feature.loginApi) - export(projects.feature.agent) - export(projects.feature.login) - export(projects.feature.subscription) - export(projects.feature.tab) + + if (appleTarget.name.startsWith("macos")) { + linkerOpts.add("-lsqlite3") + } + + commonExportedProjects.forEach { exportedProject -> + export(exportedProject) + } + + if (appleTarget.name.startsWith("ios")) { + export(projects.social.nostr) + export(projects.feature.agent) + } } } @@ -48,11 +64,10 @@ kotlin { api(projects.social.bluesky) api(projects.social.mastodon) api(projects.social.misskey) - api(projects.social.nostr) api(projects.social.pixiv) api(projects.social.vvo) api(projects.social.xqt) - api(projects.feature.agent) + api(projects.feature.loginApi) api(projects.feature.login) api(projects.feature.subscription) api(projects.feature.tab) @@ -64,6 +79,12 @@ kotlin { implementation(libs.kotlinx.immutable) } } + val iosMain by getting { + dependencies { + api(projects.social.nostr) + api(projects.feature.agent) + } + } } } diff --git a/ios-shared/src/iosMain/kotlin/dev/dimension/flare/ios/shared/IosSharedHelper.kt b/apple-shared/src/commonMain/kotlin/dev/dimension/flare/apple/shared/AppleSharedHelper.kt similarity index 67% rename from ios-shared/src/iosMain/kotlin/dev/dimension/flare/ios/shared/IosSharedHelper.kt rename to apple-shared/src/commonMain/kotlin/dev/dimension/flare/apple/shared/AppleSharedHelper.kt index 6d733b9f31..ae833b0697 100644 --- a/ios-shared/src/iosMain/kotlin/dev/dimension/flare/ios/shared/IosSharedHelper.kt +++ b/apple-shared/src/commonMain/kotlin/dev/dimension/flare/apple/shared/AppleSharedHelper.kt @@ -1,18 +1,12 @@ -package dev.dimension.flare.ios.shared +package dev.dimension.flare.apple.shared import dev.dimension.flare.common.InAppNotification import dev.dimension.flare.common.Message import dev.dimension.flare.common.SwiftOnDeviceAI import dev.dimension.flare.data.platform.AllRssTimelineLoaderFactory -import dev.dimension.flare.data.platform.BlueskyPlatformSpec -import dev.dimension.flare.data.platform.MastodonPlatformSpec -import dev.dimension.flare.data.platform.MisskeyPlatformSpec -import dev.dimension.flare.data.platform.NostrPlatformSpec -import dev.dimension.flare.data.platform.PixivPlatformSpec import dev.dimension.flare.data.platform.RssTimelineSpecs -import dev.dimension.flare.data.platform.VvoPlatformSpec -import dev.dimension.flare.data.platform.XqtPlatformSpec import dev.dimension.flare.model.PlatformRuntimeData +import dev.dimension.flare.model.PlatformSpec import dev.dimension.flare.ui.humanizer.SwiftFormatter import dev.dimension.flare.ui.render.SwiftPlatformTextRenderer import kotlinx.coroutines.CoroutineScope @@ -27,66 +21,59 @@ import org.koin.core.annotation.Single import org.koin.plugin.module.dsl.startKoin import kotlin.native.HiddenFromObjC -public object IosSharedHelper { +public object AppleSharedHelper { public fun initialize( inAppNotification: InAppNotification, swiftFormatter: SwiftFormatter, swiftPlatformTextRenderer: SwiftPlatformTextRenderer, swiftOnDeviceAI: SwiftOnDeviceAI, ) { - IosBridgeDependencies.install( + AppleBridgeDependencies.install( inAppNotification = inAppNotification, swiftFormatter = swiftFormatter, swiftPlatformTextRenderer = swiftPlatformTextRenderer, swiftOnDeviceAI = swiftOnDeviceAI, ) - startKoin() + startKoin() } } @HiddenFromObjC @KoinApplication -internal class IosKoinApplication +internal class AppleKoinApplication @HiddenFromObjC @Module @Configuration -@ComponentScan("dev.dimension.flare.ios.shared") -internal class IosKoinModule +@ComponentScan("dev.dimension.flare.apple.shared") +internal class AppleKoinModule @Single internal fun runtimeData(allRssTimelineLoaderFactory: AllRssTimelineLoaderFactory): PlatformRuntimeData = PlatformRuntimeData( - platformSpecs = - listOf( - NostrPlatformSpec, - MastodonPlatformSpec, - MisskeyPlatformSpec, - BlueskyPlatformSpec, - PixivPlatformSpec, - XqtPlatformSpec, - VvoPlatformSpec, - ), + platformSpecs = platformSpecs(), extraTimelineSpecs = RssTimelineSpecs.timelineSpecs(allRssTimelineLoaderFactory), ) +internal expect fun platformSpecs(): List + @Single internal fun inAppNotification(scope: CoroutineScope): InAppNotification = ProxyInAppNotification( - delegate = IosBridgeDependencies.inAppNotification(), + delegate = AppleBridgeDependencies.inAppNotification(), scope = scope, ) @Single -internal fun swiftFormatter(): SwiftFormatter = IosBridgeDependencies.swiftFormatter() +internal fun swiftFormatter(): SwiftFormatter = AppleBridgeDependencies.swiftFormatter() @Single -internal fun swiftPlatformTextRenderer(): SwiftPlatformTextRenderer = IosBridgeDependencies.swiftPlatformTextRenderer() +internal fun swiftPlatformTextRenderer(): SwiftPlatformTextRenderer = AppleBridgeDependencies.swiftPlatformTextRenderer() @Single -internal fun swiftOnDeviceAI(): SwiftOnDeviceAI = IosBridgeDependencies.swiftOnDeviceAI() +internal fun swiftOnDeviceAI(): SwiftOnDeviceAI = AppleBridgeDependencies.swiftOnDeviceAI() -private object IosBridgeDependencies { +private object AppleBridgeDependencies { private var inAppNotification: InAppNotification? = null private var swiftFormatter: SwiftFormatter? = null private var swiftPlatformTextRenderer: SwiftPlatformTextRenderer? = null @@ -105,18 +92,18 @@ private object IosBridgeDependencies { } fun inAppNotification(): InAppNotification = - requireNotNull(inAppNotification) { "IosSharedHelper.initialize must install InAppNotification before Koin starts." } + requireNotNull(inAppNotification) { "AppleSharedHelper.initialize must install InAppNotification before Koin starts." } fun swiftFormatter(): SwiftFormatter = - requireNotNull(swiftFormatter) { "IosSharedHelper.initialize must install SwiftFormatter before Koin starts." } + requireNotNull(swiftFormatter) { "AppleSharedHelper.initialize must install SwiftFormatter before Koin starts." } fun swiftPlatformTextRenderer(): SwiftPlatformTextRenderer = requireNotNull(swiftPlatformTextRenderer) { - "IosSharedHelper.initialize must install SwiftPlatformTextRenderer before Koin starts." + "AppleSharedHelper.initialize must install SwiftPlatformTextRenderer before Koin starts." } fun swiftOnDeviceAI(): SwiftOnDeviceAI = - requireNotNull(swiftOnDeviceAI) { "IosSharedHelper.initialize must install SwiftOnDeviceAI before Koin starts." } + requireNotNull(swiftOnDeviceAI) { "AppleSharedHelper.initialize must install SwiftOnDeviceAI before Koin starts." } } private class ProxyInAppNotification( diff --git a/apple-shared/src/iosMain/kotlin/dev/dimension/flare/apple/shared/PlatformSpecs.ios.kt b/apple-shared/src/iosMain/kotlin/dev/dimension/flare/apple/shared/PlatformSpecs.ios.kt new file mode 100644 index 0000000000..0996422651 --- /dev/null +++ b/apple-shared/src/iosMain/kotlin/dev/dimension/flare/apple/shared/PlatformSpecs.ios.kt @@ -0,0 +1,21 @@ +package dev.dimension.flare.apple.shared + +import dev.dimension.flare.data.platform.BlueskyPlatformSpec +import dev.dimension.flare.data.platform.MastodonPlatformSpec +import dev.dimension.flare.data.platform.MisskeyPlatformSpec +import dev.dimension.flare.data.platform.NostrPlatformSpec +import dev.dimension.flare.data.platform.PixivPlatformSpec +import dev.dimension.flare.data.platform.VvoPlatformSpec +import dev.dimension.flare.data.platform.XqtPlatformSpec +import dev.dimension.flare.model.PlatformSpec + +internal actual fun platformSpecs(): List = + listOf( + NostrPlatformSpec, + MastodonPlatformSpec, + MisskeyPlatformSpec, + BlueskyPlatformSpec, + PixivPlatformSpec, + XqtPlatformSpec, + VvoPlatformSpec, + ) diff --git a/apple-shared/src/macosMain/kotlin/dev/dimension/flare/apple/shared/PlatformSpecs.macos.kt b/apple-shared/src/macosMain/kotlin/dev/dimension/flare/apple/shared/PlatformSpecs.macos.kt new file mode 100644 index 0000000000..5186bcaf86 --- /dev/null +++ b/apple-shared/src/macosMain/kotlin/dev/dimension/flare/apple/shared/PlatformSpecs.macos.kt @@ -0,0 +1,19 @@ +package dev.dimension.flare.apple.shared + +import dev.dimension.flare.data.platform.BlueskyPlatformSpec +import dev.dimension.flare.data.platform.MastodonPlatformSpec +import dev.dimension.flare.data.platform.MisskeyPlatformSpec +import dev.dimension.flare.data.platform.PixivPlatformSpec +import dev.dimension.flare.data.platform.VvoPlatformSpec +import dev.dimension.flare.data.platform.XqtPlatformSpec +import dev.dimension.flare.model.PlatformSpec + +internal actual fun platformSpecs(): List = + listOf( + MastodonPlatformSpec, + MisskeyPlatformSpec, + BlueskyPlatformSpec, + PixivPlatformSpec, + XqtPlatformSpec, + VvoPlatformSpec, + ) diff --git a/iosApp/flare/Common/AppleClickContext.swift b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleCore/AppleUriLauncher.swift similarity index 50% rename from iosApp/flare/Common/AppleClickContext.swift rename to appleApp/Shared/FlareAppleCore/Sources/FlareAppleCore/AppleUriLauncher.swift index 655fc169bf..b4b06d0caa 100644 --- a/iosApp/flare/Common/AppleClickContext.swift +++ b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleCore/AppleUriLauncher.swift @@ -1,13 +1,14 @@ -import SwiftUI import KotlinSharedUI +import SwiftUI + +public final class AppleUriLauncher: UriLauncher { + private let openUrl: OpenURLAction -class AppleUriLauncher: UriLauncher { - let openUrl: OpenURLAction - init(openUrl: OpenURLAction) { + public init(openUrl: OpenURLAction) { self.openUrl = openUrl } - func launch(uri: String) { + public func launch(uri: String) { if let url = URL(string: uri) { openUrl.callAsFunction(url) } diff --git a/iosApp/flare/Common/Formatter.swift b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleCore/Formatter.swift similarity index 57% rename from iosApp/flare/Common/Formatter.swift rename to appleApp/Shared/FlareAppleCore/Sources/FlareAppleCore/Formatter.swift index 54b6dd4c10..72902be904 100644 --- a/iosApp/flare/Common/Formatter.swift +++ b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleCore/Formatter.swift @@ -1,12 +1,13 @@ import Foundation import KotlinSharedUI -class Formatter: SwiftFormatter { +public final class Formatter: SwiftFormatter, @unchecked Sendable { private init() {} - - static let shared = Formatter() - nonisolated func formatNumber(number: Int64) -> String { - return Int(number) + + public static let shared = Formatter() + + nonisolated public func formatNumber(number: Int64) -> String { + Int(number) .formatted( .number .notation(.compactName) diff --git a/appleApp/Shared/FlareAppleCore/Sources/FlareAppleCore/FoundationModelOnDeviceAI.swift b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleCore/FoundationModelOnDeviceAI.swift new file mode 100644 index 0000000000..1415c543e0 --- /dev/null +++ b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleCore/FoundationModelOnDeviceAI.swift @@ -0,0 +1,50 @@ +import Foundation +import KotlinSharedUI + +#if canImport(FoundationModels) +import FoundationModels +#endif + +public final class FoundationModelOnDeviceAI: SwiftOnDeviceAI, @unchecked Sendable { + private init() {} + + public static let shared = FoundationModelOnDeviceAI() + + public func __isAvailable() async throws -> KotlinBoolean { + #if canImport(FoundationModels) + if #available(iOS 26.0, macOS 26.0, *) { + return KotlinBoolean(bool: SystemLanguageModel.default.isAvailable) + } + #endif + + return KotlinBoolean(bool: false) + } + + public func __translate(source: String, targetLanguage: String, prompt: String) async throws -> String? { + await generateText(prompt: prompt) + } + + public func __tldr(source: String, targetLanguage: String, prompt: String) async throws -> String? { + await generateText(prompt: prompt) + } + + private func generateText(prompt: String) async -> String? { + guard ((try? await __isAvailable())?.boolValue ?? false) else { + return nil + } + + #if canImport(FoundationModels) + if #available(iOS 26.0, macOS 26.0, *) { + do { + let session = LanguageModelSession() + let response = try await session.respond(to: prompt) + return response.content + } catch { + return nil + } + } + #endif + + return nil + } +} diff --git a/iosApp/flare/Common/KotlinByteArray+Data.swift b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleCore/KotlinByteArray+Data.swift similarity index 89% rename from iosApp/flare/Common/KotlinByteArray+Data.swift rename to appleApp/Shared/FlareAppleCore/Sources/FlareAppleCore/KotlinByteArray+Data.swift index e4a72c09d4..24c1920761 100644 --- a/iosApp/flare/Common/KotlinByteArray+Data.swift +++ b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleCore/KotlinByteArray+Data.swift @@ -1,7 +1,7 @@ import Foundation import KotlinSharedUI -extension KotlinByteArray { +public extension KotlinByteArray { static func from(data: Data) -> KotlinByteArray { let swiftByteArray = [UInt8](data) return swiftByteArray @@ -9,6 +9,6 @@ extension KotlinByteArray { .enumerated() .reduce(into: KotlinByteArray(size: Int32(swiftByteArray.count))) { result, row in result.set(index: Int32(row.offset), value: row.element) - } + } } } diff --git a/iosApp/flare/Common/Array+Cast.swift b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleCore/KotlinCollections.swift similarity index 78% rename from iosApp/flare/Common/Array+Cast.swift rename to appleApp/Shared/FlareAppleCore/Sources/FlareAppleCore/KotlinCollections.swift index f40080b03a..19c019c489 100644 --- a/iosApp/flare/Common/Array+Cast.swift +++ b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleCore/KotlinCollections.swift @@ -1,6 +1,6 @@ import Foundation -extension NSArray { +public extension NSArray { func cast(_ type: T.Type) -> [T] { compactMap { $0 as? T } } diff --git a/appleApp/Shared/FlareAppleCore/Sources/FlareAppleCore/KotlinPresenter.swift b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleCore/KotlinPresenter.swift new file mode 100644 index 0000000000..dc9613f3f1 --- /dev/null +++ b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleCore/KotlinPresenter.swift @@ -0,0 +1,33 @@ +@preconcurrency import Combine +import Foundation +@preconcurrency import KotlinSharedUI + +// Keep PresenterBase owned by a stable ObservableObject so SwiftUI view reloads +// do not recreate heavy Kotlin presenters. +public final class KotlinPresenter: ObservableObject { + private var subscribers = Set() + + public let presenter: PresenterBase + public let key = UUID().uuidString + + public init(presenter: PresenterBase) { + self.presenter = presenter + self.state = presenter.models.value + self.presenter.models.toPublisher() + .receive(on: DispatchQueue.main) + .sink { [weak self] newState in + guard let self, self.state !== newState else { return } + self.state = newState + } + .store(in: &subscribers) + } + + @Published public var state: T + + deinit { + subscribers.forEach { cancellable in + cancellable.cancel() + } + presenter.close() + } +} diff --git a/iosApp/flare/Common/OPMLFile.swift b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleCore/OPMLFile.swift similarity index 54% rename from iosApp/flare/Common/OPMLFile.swift rename to appleApp/Shared/FlareAppleCore/Sources/FlareAppleCore/OPMLFile.swift index bb5c02412d..8b8d473883 100644 --- a/iosApp/flare/Common/OPMLFile.swift +++ b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleCore/OPMLFile.swift @@ -1,43 +1,43 @@ import SwiftUI import UniformTypeIdentifiers -extension UTType { +public extension UTType { nonisolated static var opml: UTType { UTType(exportedAs: "dev.dimension.flare.opml", conformingTo: .xml) } } -struct OPMLFile: FileDocument { - nonisolated static var readableContentTypes: [UTType] { [.opml] } +public struct OPMLFile: FileDocument { + public nonisolated static var readableContentTypes: [UTType] { [.opml] } - var text = "" + public var text = "" - init(initialText: String = "") { + public init(initialText: String = "") { text = initialText } - init(configuration: ReadConfiguration) throws { + public init(configuration: ReadConfiguration) throws { if let data = configuration.file.regularFileContents { text = String(decoding: data, as: UTF8.self) } } - func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper { + public func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper { let data = Data(text.utf8) return FileWrapper(regularFileWithContents: data) } } -struct TextDocument: FileDocument { - var text: String +public struct TextDocument: FileDocument { + public var text: String - static var readableContentTypes: [UTType] { [.plainText] } + public static var readableContentTypes: [UTType] { [.plainText] } - init(text: String = "") { + public init(text: String = "") { self.text = text } - init(configuration: ReadConfiguration) throws { + public init(configuration: ReadConfiguration) throws { guard let data = configuration.file.regularFileContents, let string = String(data: data, encoding: .utf8) else { @@ -46,7 +46,7 @@ struct TextDocument: FileDocument { text = string } - func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper { + public func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper { let data = Data(text.utf8) return FileWrapper(regularFileWithContents: data) } diff --git a/iosApp/flare/Common/PlatformTextRenderer.swift b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleCore/PlatformTextRenderer.swift similarity index 74% rename from iosApp/flare/Common/PlatformTextRenderer.swift rename to appleApp/Shared/FlareAppleCore/Sources/FlareAppleCore/PlatformTextRenderer.swift index eb4d63e302..86b2e76faa 100644 --- a/iosApp/flare/Common/PlatformTextRenderer.swift +++ b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleCore/PlatformTextRenderer.swift @@ -1,24 +1,29 @@ +import Foundation +@preconcurrency import KotlinSharedUI import SwiftUI + +#if canImport(UIKit) import UIKit -@preconcurrency import KotlinSharedUI +#elseif canImport(AppKit) +import AppKit +#endif -class PlatformTextContent: NSObject {} +public class PlatformTextContent: NSObject {} -final class PlatformTextTextContent: PlatformTextContent { - let runs: [PlatformTextRun] - let alignment: TextAlignment? - let isBlockQuote: Bool +public final class PlatformTextTextContent: PlatformTextContent { + public let runs: [PlatformTextRun] + public let alignment: TextAlignment? + public let isBlockQuote: Bool /// `true` when any run carries a tappable link. UIKit renderers use this /// to decide whether the block needs a `UITextView` (link interaction) /// or can fall back to a plain `UILabel`. - let hasLink: Bool + public let hasLink: Bool /// `true` when any run is an inline image (custom emoji, inline image). - /// `UILabel` *can* render `NSTextAttachment`, but to keep the fast path - /// simple and predictable we still route these blocks through - /// `UITextView`. - let hasInlineImage: Bool + /// `UILabel` can render `NSTextAttachment`, but to keep the fast path + /// simple and predictable we still route these blocks through `UITextView`. + public let hasInlineImage: Bool - init( + public init( runs: [PlatformTextRun], alignment: TextAlignment?, isBlockQuote: Bool, @@ -34,45 +39,45 @@ final class PlatformTextTextContent: PlatformTextContent { } } -final class PlatformTextBlockImageContent: PlatformTextContent { - let url: String - let href: String? +public final class PlatformTextBlockImageContent: PlatformTextContent { + public let url: String + public let href: String? - init(url: String, href: String?) { + public init(url: String, href: String?) { self.url = url self.href = href super.init() } } -class PlatformTextRun: NSObject {} +public class PlatformTextRun: NSObject {} -final class PlatformTextAttributedRun: PlatformTextRun { - let attributedText: NSAttributedString - let text: AttributedString +public final class PlatformTextAttributedRun: PlatformTextRun { + public let attributedText: NSAttributedString + public let text: AttributedString - init(attributedText: NSAttributedString, text: AttributedString) { + public init(attributedText: NSAttributedString, text: AttributedString) { self.attributedText = attributedText self.text = text super.init() } } -final class PlatformTextStyleDescriptor: NSObject { - let link: String? - let bold: Bool - let italic: Bool - let strikethrough: Bool - let monospace: Bool - let code: Bool - let underline: Bool - let small: Bool - let time: Bool - let headingLevel: Int? - let isBlockQuote: Bool - let isFigCaption: Bool - - init(style: RenderTextStyle, block: RenderBlockStyle) { +public final class PlatformTextStyleDescriptor: NSObject { + public let link: String? + public let bold: Bool + public let italic: Bool + public let strikethrough: Bool + public let monospace: Bool + public let code: Bool + public let underline: Bool + public let small: Bool + public let time: Bool + public let headingLevel: Int? + public let isBlockQuote: Bool + public let isFigCaption: Bool + + public init(style: RenderTextStyle, block: RenderBlockStyle) { link = style.link bold = style.bold italic = style.italic @@ -89,27 +94,27 @@ final class PlatformTextStyleDescriptor: NSObject { } } -extension NSAttributedString.Key { +public extension NSAttributedString.Key { static let platformTextStyleDescriptor = NSAttributedString.Key("dev.dimension.flare.platformTextStyleDescriptor") } -final class PlatformTextImageRun: PlatformTextRun { - let url: String - let alt: String +public final class PlatformTextImageRun: PlatformTextRun { + public let url: String + public let alt: String - init(url: String, alt: String) { + public init(url: String, alt: String) { self.url = url self.alt = alt super.init() } } -final class PlatformTextRenderer: SwiftPlatformTextRenderer { - static let shared = PlatformTextRenderer() +public final class PlatformTextRenderer: SwiftPlatformTextRenderer, @unchecked Sendable { + public static let shared = PlatformTextRenderer() private init() {} - func render(renderRuns: [RenderContent]) -> [Any] { + public func render(renderRuns: [RenderContent]) -> [Any] { dispatchPrecondition(condition: .onQueue(.main)) let context = RenderContext() @@ -279,7 +284,7 @@ private final class RenderContext { return nil } - var font = Font.system(size: style.small ? UIFont.smallSystemFontSize : UIFont.systemFontSize) + var font = Font.system(size: style.small ? Self.platformSmallSystemFontSize : Self.platformSystemFontSize) if style.bold { font = font.weight(.bold) } @@ -297,4 +302,24 @@ private final class RenderContext { return nil } } + + private static var platformSmallSystemFontSize: CGFloat { + #if canImport(UIKit) + return UIFont.smallSystemFontSize + #elseif canImport(AppKit) + return NSFont.smallSystemFontSize + #else + return 13 + #endif + } + + private static var platformSystemFontSize: CGFloat { + #if canImport(UIKit) + return UIFont.systemFontSize + #elseif canImport(AppKit) + return NSFont.systemFontSize + #else + return 17 + #endif + } } diff --git a/appleApp/Shared/FlareAppleCore/Sources/FlareAppleCore/PresentationMappings.swift b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleCore/PresentationMappings.swift new file mode 100644 index 0000000000..1dbc3f8e8c --- /dev/null +++ b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleCore/PresentationMappings.swift @@ -0,0 +1,311 @@ +import AppleFontAwesome +import Foundation +import KotlinSharedUI +import SwiftUI + +public func localizedPresentationString( + _ key: String, + fallback: String, + arguments: [String] = [] +) -> String { + let value = Bundle.main.localizedString(forKey: key, value: fallback, table: nil) + guard !arguments.isEmpty else { return value } + return String(format: value, arguments: arguments.map { $0 as CVarArg }) +} + +public extension UiText { + var text: String { + switch onEnum(of: self) { + case .localized(let localized): + localized.string.text + case .raw(let raw): + raw.string + } + } +} + +public extension UiStrings { + var text: String { + switch self { + case .home: + localizedPresentationString("home_tab_home_title", fallback: "Home") + case .notifications: + localizedPresentationString("home_tab_notifications_title", fallback: "Notifications") + case .discover: + localizedPresentationString("home_tab_discover_title", fallback: "Discover") + case .me: + localizedPresentationString("home_tab_me_title", fallback: "Me") + case .settings: + localizedPresentationString("settings_title", fallback: "Settings") + case .mastodonLocal: + localizedPresentationString("mastodon_tab_local_title", fallback: "Local") + case .mastodonPublic: + localizedPresentationString("mastodon_tab_public_title", fallback: "Public") + case .featured: + localizedPresentationString("home_tab_featured_title", fallback: "Featured") + case .bookmark: + localizedPresentationString("home_tab_bookmarks_title", fallback: "Bookmarks") + case .favourite: + localizedPresentationString("home_tab_favorite_title", fallback: "Favorites") + case .list: + localizedPresentationString("home_tab_list_title", fallback: "Lists") + case .feeds: + localizedPresentationString("home_tab_feeds_title", fallback: "Feeds") + case .directMessage: + localizedPresentationString("dm_list_title", fallback: "Direct Messages") + case .rss: + localizedPresentationString("rss_title", fallback: "Subscriptions") + case .antenna: + localizedPresentationString("antenna_title", fallback: "Antennas") + case .mixedTimeline: + localizedPresentationString("mixed_timeline_title", fallback: "Mixed") + case .social: + localizedPresentationString("social_title", fallback: "Social") + case .liked: + localizedPresentationString("liked_tab_title", fallback: "Liked") + case .allRssFeeds: + localizedPresentationString("all_rss_feeds_title", fallback: "All Subscriptions") + case .posts: + localizedPresentationString("posts_title", fallback: "Posts") + case .channel: + localizedPresentationString("channel_title", fallback: "Channel") + case .default: + localizedPresentationString("tab_settings_default", fallback: "Default") + case .login: + localizedPresentationString("login_button", fallback: "Log in") + case .verify: + localizedPresentationString("verify_button", fallback: "Verify") + case .cancel: + localizedPresentationString("cancel_button", fallback: "Cancel") + case .next: + localizedPresentationString("service_select_next_button", fallback: "Next") + case .username: + localizedPresentationString("bluesky_login_username_hint", fallback: "Username") + case .password: + localizedPresentationString("bluesky_login_password_hint", fallback: "Password") + case .otp: + localizedPresentationString("bluesky_login_auth_factor_token_hint", fallback: "One-time Password") + case .oauthLogin: + localizedPresentationString("bluesky_login_oauth_button", fallback: "OAuth Login") + case .passwordLogin: + localizedPresentationString("bluesky_login_use_password_button", fallback: "Password Login") + case .qrConnect: + localizedPresentationString("nostr_login_qr_button", fallback: "QR Connect") + case .credentialImport: + localizedPresentationString("nostr_login_title", fallback: "Credential Import") + case .externalSigner: + localizedPresentationString("nostr_login_amber_button", fallback: "External Signer") + case .webCookieLogin: + localizedPresentationString("login_button", fallback: "Log in") + case .nostrLoginAccount: + localizedPresentationString("nostr_login_account_hint", fallback: "Nostr Account") + case .pixivRankingWeek: + localizedPresentationString("pixiv_ranking_week_title", fallback: "Weekly Ranking") + case .pixivRankingMonth: + localizedPresentationString("pixiv_ranking_month_title", fallback: "Monthly Ranking") + case .pixivRankingDayMale: + localizedPresentationString("pixiv_ranking_day_male_title", fallback: "Male Ranking") + case .pixivRankingDayFemale: + localizedPresentationString("pixiv_ranking_day_female_title", fallback: "Female Ranking") + case .pixivRankingWeekOriginal: + localizedPresentationString("pixiv_ranking_week_original_title", fallback: "Original Ranking") + case .pixivRankingWeekRookie: + localizedPresentationString("pixiv_ranking_week_rookie_title", fallback: "Rookie Ranking") + case .pixivRankingDayManga: + localizedPresentationString("pixiv_ranking_day_manga_title", fallback: "Manga Ranking") + case .illustrations: + localizedPresentationString("illustrations_title", fallback: "Illustrations") + case .manga: + localizedPresentationString("manga_title", fallback: "Manga") + case .following: + localizedPresentationString("misskey_channel_tab_following", fallback: "Following") + case .postsWithReplies: + localizedPresentationString("posts_with_replies_title", fallback: "Posts & Replies") + case .media: + localizedPresentationString("media_title", fallback: "Media") + } + } +} + +public extension ActionMenuItemText { + var resolvedString: String { + switch onEnum(of: self) { + case .raw(let raw): + raw.text + case .localized(let localized): + switch localized.type { + case .like: + localizedPresentationString("like", fallback: "Like") + case .unlike: + localizedPresentationString("unlike", fallback: "Unlike") + case .retweet: + localizedPresentationString("retweet", fallback: "Retweet") + case .unretweet: + localizedPresentationString("retweet_remove", fallback: "Remove retweet") + case .reply: + localizedPresentationString("reply", fallback: "Reply") + case .comment: + localizedPresentationString("comment", fallback: "Comment") + case .quote: + localizedPresentationString("quote", fallback: "Quote") + case .bookmark: + localizedPresentationString("bookmark_add", fallback: "Add bookmark") + case .unbookmark: + localizedPresentationString("bookmark_remove", fallback: "Remove bookmark") + case .more: + localizedPresentationString("more", fallback: "More") + case .delete: + localizedPresentationString("delete", fallback: "Delete") + case .report: + localizedPresentationString("report", fallback: "Report") + case .react: + localizedPresentationString("reaction_add", fallback: "Add reaction") + case .share: + localizedPresentationString("share", fallback: "Share") + case .fxShare: + localizedPresentationString("fx_share", fallback: "FX Share") + case .unReact: + localizedPresentationString("reaction_remove", fallback: "Remove reaction") + case .editUserList: + localizedPresentationString("edit_user_in_list", fallback: "Edit user in list") + case .sendMessage: + localizedPresentationString("send_message", fallback: "Send message") + case .mute: + localizedPresentationString("mute", fallback: "Mute") + case .unMute: + localizedPresentationString("unmute", fallback: "Unmute") + case .block: + localizedPresentationString("block", fallback: "Block") + case .unBlock: + localizedPresentationString("unblock", fallback: "Unblock") + case .blockWithHandleParameter: + localizedPresentationString( + "block_user_with_handle %@", + fallback: "Block %@", + arguments: [localized.parameters.first ?? ""] + ) + case .muteWithHandleParameter: + localizedPresentationString( + "mute_user_with_handle %@", + fallback: "Mute %@", + arguments: [localized.parameters.first ?? ""] + ) + case .acceptFollowRequest: + localizedPresentationString("accept_follow_request", fallback: "Accept follow request") + case .rejectFollowRequest: + localizedPresentationString("reject_follow_request", fallback: "Reject follow request") + case .retryTranslation: + localizedPresentationString("Retry translation", fallback: "Retry translation") + case .translate: + localizedPresentationString("Translate", fallback: "Translate") + case .showOriginal: + localizedPresentationString("Show original", fallback: "Show original") + case .favorite: + localizedPresentationString("Favourite", fallback: "Favourite") + case .unFavorite: + localizedPresentationString("Unfavourite", fallback: "Unfavourite") + } + } + } +} + +public extension UiIcon { + var image: Image { + Image(fontAwesome: fontAwesomeIcon) + } + + var fontAwesomeIcon: FontAwesomeIcon { + switch self { + case .home: + .house + case .notification: + .bell + case .search: + .magnifyingGlass + case .profile: + .circleUser + case .settings: + .gear + case .local: + .users + case .world: + .globe + case .featured: + .rectangleList + case .bookmark: + .bookmark + case .unbookmark: + .bookmarkFill + case .heart, .like: + .heart + case .unlike: + .heartFill + case .twitter: + .twitter + case .x: + .xTwitter + case .mastodon: + .mastodon + case .misskey: + .misskey + case .bluesky: + .bluesky + case .nostr: + .nostr + case .weibo: + .weibo + case .pixiv: + .pixiv + case .list: + .list + case .feeds, .rss: + .squareRss + case .messages, .chatMessage: + .message + case .channel: + .tv + case .retweet, .unretweet: + .retweet + case .reply, .quote: + .reply + case .comment: + .commentDots + case .more: + .ellipsis + case .moreVerticel: + .ellipsisVertical + case .delete: + .trash + case .report, .info: + .circleInfo + case .react: + .plus + case .unReact: + .minus + case .share: + .shareNodes + case .mute, .unMute: + .volumeXmark + case .block, .unBlock: + .userSlash + case .follow: + .userPlus + case .favourite: + .starFill + case .unFavourite: + .star + case .mention: + .at + case .poll: + .squarePollHorizontal + case .edit: + .pen + case .pin: + .thumbtack + case .check: + .check + case .translate: + .language + } + } +} diff --git a/iosApp/flare/UI/Component/StateView.swift b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleCore/StateView.swift similarity index 62% rename from iosApp/flare/UI/Component/StateView.swift rename to appleApp/Shared/FlareAppleCore/Sources/FlareAppleCore/StateView.swift index 28bb691a33..e54d629919 100644 --- a/iosApp/flare/UI/Component/StateView.swift +++ b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleCore/StateView.swift @@ -1,13 +1,27 @@ -import Foundation import KotlinSharedUI import SwiftUI -struct StateView: View { - let state: UiState - @ViewBuilder var successContent: (T) -> SuccessContent - @ViewBuilder var errorContent: (KotlinThrowable) -> ErrorContent - @ViewBuilder var loadingContent: () -> LoadingContent - var body: some View { +public struct StateView< + T: AnyObject, SuccessContent: View, ErrorContent: View, LoadingContent: View +>: View { + private let state: UiState + private let successContent: (T) -> SuccessContent + private let errorContent: (KotlinThrowable) -> ErrorContent + private let loadingContent: () -> LoadingContent + + public init( + state: UiState, + @ViewBuilder successContent: @escaping (T) -> SuccessContent, + @ViewBuilder errorContent: @escaping (KotlinThrowable) -> ErrorContent, + @ViewBuilder loadingContent: @escaping () -> LoadingContent + ) { + self.state = state + self.successContent = successContent + self.errorContent = errorContent + self.loadingContent = loadingContent + } + + public var body: some View { switch onEnum(of: state) { case .error(let error): errorContent(error.throwable) @@ -20,8 +34,7 @@ struct StateView, @ViewBuilder successContent: @escaping (T) -> SuccessContent ) where LoadingContent == EmptyView, ErrorContent == EmptyView { @@ -33,7 +46,7 @@ extension StateView { ) } - init( + public init( state: UiState, @ViewBuilder successContent: @escaping (T) -> SuccessContent, @ViewBuilder loadingContent: @escaping () -> LoadingContent @@ -46,7 +59,7 @@ extension StateView { ) } - init( + public init( state: UiState, @ViewBuilder successContent: @escaping (T) -> SuccessContent, @ViewBuilder errorContent: @escaping (KotlinThrowable) -> ErrorContent @@ -60,10 +73,11 @@ extension StateView { } } - extension View { - func onSuccessOf(of state: UiState, data: @escaping (T) -> ()) -> some View { - self.onChange(of: state) { oldValue, newValue in + public func onSuccessOf(of state: UiState, data: @escaping (T) -> Void) + -> some View + { + onChange(of: state) { _, newValue in switch onEnum(of: newValue) { case .success(let success): data(success.data) diff --git a/iosApp/flare/Common/View+If.swift b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleCore/View+If.swift similarity index 95% rename from iosApp/flare/Common/View+If.swift rename to appleApp/Shared/FlareAppleCore/Sources/FlareAppleCore/View+If.swift index 62298bfdc7..0f36f1db9b 100644 --- a/iosApp/flare/Common/View+If.swift +++ b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleCore/View+If.swift @@ -1,6 +1,6 @@ import SwiftUI -extension View { +public extension View { @ViewBuilder func `if`( _ condition: Bool, @@ -13,7 +13,7 @@ extension View { elseTransform(self) } } - + @ViewBuilder func `if`( _ condition: Bool, diff --git a/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/AdaptiveTimelineCard.swift b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/AdaptiveTimelineCard.swift new file mode 100644 index 0000000000..41bc314b9f --- /dev/null +++ b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/AdaptiveTimelineCard.swift @@ -0,0 +1,57 @@ +import SwiftUI +import KotlinSharedUI + +#if os(macOS) +import AppKit +#endif + +public struct AdaptiveTimelineCard: View { + @Environment(\.timelineAppearance.timelineDisplayMode) private var timelineDisplayMode + @Environment(\.isMultipleColumn) private var isMultipleColumn + private let index: Int + private let totalCount: Int + private let content: () -> Content + + public init( + index: Int, + totalCount: Int, + @ViewBuilder content: @escaping () -> Content + ) { + self.index = index + self.totalCount = totalCount + self.content = content + } + + public var body: some View { + #if os(macOS) + if isMultipleColumn || timelineDisplayMode == .card { + content() + .background(Color(nsColor: .controlBackgroundColor)) + .clipShape(RoundedRectangle(cornerRadius: 18)) + .padding(.horizontal, isMultipleColumn ? 6 : 12) + .padding(.vertical, 6) + } else { + VStack(spacing: 0) { + content() + if index < totalCount - 1 { + Divider() + } + } + } + #else + if isMultipleColumn || !(timelineDisplayMode == .plain) { + ListCardView(index: index, totalCount: totalCount) { + content() + } + .padding(.horizontal) + } else { + VStack(spacing: 0) { + content() + if totalCount <= 0 || index < totalCount - 1 { + Divider() + } + } + } + #endif + } +} diff --git a/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/BackportLabelStyle.swift b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/BackportLabelStyle.swift new file mode 100644 index 0000000000..28f4875518 --- /dev/null +++ b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/BackportLabelStyle.swift @@ -0,0 +1,28 @@ +import SwiftUI +import SwiftUIBackports + +public extension Backport where Content: View { + @ViewBuilder + func flareLabelIconToTitleSpacing(_ spacing: CGFloat) -> some View { + #if os(iOS) + if #available(iOS 26.0, *) { + content.labelIconToTitleSpacing(spacing) + } else { + content.labelStyle(FlareBackportLabelStyle(spacing: spacing)) + } + #else + content.labelStyle(FlareBackportLabelStyle(spacing: spacing)) + #endif + } +} + +private struct FlareBackportLabelStyle: LabelStyle { + let spacing: CGFloat + + func makeBody(configuration: Configuration) -> some View { + HStack(spacing: spacing) { + configuration.icon + configuration.title + } + } +} diff --git a/iosApp/flare/UI/Screen/DeepLinkAccountPicker.swift b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/DeepLinkAccountPickerView.swift similarity index 65% rename from iosApp/flare/UI/Screen/DeepLinkAccountPicker.swift rename to appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/DeepLinkAccountPickerView.swift index 84d86b8649..2ee7eddd4d 100644 --- a/iosApp/flare/UI/Screen/DeepLinkAccountPicker.swift +++ b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/DeepLinkAccountPickerView.swift @@ -1,15 +1,26 @@ -import SwiftUI +import FlareAppleCore import KotlinSharedUI -import SwiftUIBackports +import SwiftUI -struct DeepLinkAccountPicker: View { +public struct DeepLinkAccountPickerView: View { @Environment(\.dismiss) private var dismiss @Environment(\.openURL) private var openURL - let originalUrl: String - let data: [MicroBlogKey : Route] - let onNavigate: (Route) -> Void - - var body: some View { + + private let originalUrl: String + private let data: [MicroBlogKey: Route] + private let onNavigate: (Route) -> Void + + public init( + originalUrl: String, + data: [MicroBlogKey: Route], + onNavigate: @escaping (Route) -> Void + ) { + self.originalUrl = originalUrl + self.data = data + self.onNavigate = onNavigate + } + + public var body: some View { List { ForEach(data.keys.sorted(by: { $0.id < $1.id }), id: \.self) { userKey in if let route = data[userKey] { @@ -17,11 +28,12 @@ struct DeepLinkAccountPicker: View { onNavigate(route) dismiss() } label: { - UserItemView(userKey: userKey) + DeepLinkAccountRow(userKey: userKey) } .buttonStyle(.plain) } } + Button { let routeLink = DeeplinkRoute.OpenLinkDirectly(url: originalUrl).toUri() if let url = URL(string: routeLink) { @@ -32,14 +44,12 @@ struct DeepLinkAccountPicker: View { Label { Text("deep_link_account_picker_open_in_browser") } icon: { - Image(.faGlobe) + Image(systemName: "globe") } } .buttonStyle(.plain) } .navigationTitle("deep_link_account_picker_title") - .backport - .navigationSubtitle("deep_link_account_picker_subtitle") .toolbar { ToolbarItem(placement: .cancellationAction) { Button( @@ -50,7 +60,7 @@ struct DeepLinkAccountPicker: View { Label { Text("Cancel") } icon: { - Image("fa-xmark") + Image(systemName: "xmark") } } } @@ -58,13 +68,20 @@ struct DeepLinkAccountPicker: View { } } -private struct UserItemView : View { +private struct DeepLinkAccountRow: View { @StateObject private var presenter: KotlinPresenter - + init(userKey: MicroBlogKey) { - self._presenter = .init(wrappedValue: .init(presenter: UserPresenter(accountType: AccountType.Specific(accountKey: userKey), userKey: nil))) + _presenter = .init( + wrappedValue: .init( + presenter: UserPresenter( + accountType: AccountType.Specific(accountKey: userKey), + userKey: nil + ) + ) + ) } - + var body: some View { StateView(state: presenter.state.user) { user in UserCompatView(data: user) diff --git a/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/FlareColors.swift b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/FlareColors.swift new file mode 100644 index 0000000000..a6d22fccc0 --- /dev/null +++ b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/FlareColors.swift @@ -0,0 +1,65 @@ +import SwiftUI + +#if os(iOS) +import UIKit +#elseif os(macOS) +import AppKit +#endif + +public extension Color { + static var flareSystemBackground: Color { + #if os(iOS) + Color(.systemBackground) + #elseif os(macOS) + Color(nsColor: .windowBackgroundColor) + #endif + } + + static var flareSystemGroupedBackground: Color { + #if os(iOS) + Color(.systemGroupedBackground) + #elseif os(macOS) + Color(nsColor: .windowBackgroundColor) + #endif + } + + static var flareSecondarySystemBackground: Color { + #if os(iOS) + Color(.secondarySystemBackground) + #elseif os(macOS) + Color(nsColor: .alternatingContentBackgroundColors[1]) + #endif + } + + static var flareSecondarySystemGroupedBackground: Color { + #if os(iOS) + Color(.secondarySystemGroupedBackground) + #elseif os(macOS) + Color(nsColor: .controlBackgroundColor) + #endif + } + + static var flareSeparator: Color { + #if os(iOS) + Color(.separator) + #elseif os(macOS) + Color(nsColor: .separatorColor) + #endif + } + + static var flareLabel: Color { + #if os(iOS) + Color(.label) + #elseif os(macOS) + Color(nsColor: .labelColor) + #endif + } + + static var flareSecondaryLabel: Color { + #if os(iOS) + Color(.secondaryLabel) + #elseif os(macOS) + Color(nsColor: .secondaryLabelColor) + #endif + } +} diff --git a/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/FlareEnvironmentValues.swift b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/FlareEnvironmentValues.swift new file mode 100644 index 0000000000..e8e24839d5 --- /dev/null +++ b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/FlareEnvironmentValues.swift @@ -0,0 +1,72 @@ +import SwiftUI +import KotlinSharedUI + +public enum NetworkKind: Equatable { + case wifi + case cellular + + public var description: String { + switch self { + case .wifi: + "Wi-Fi" + case .cellular: + "Cellular" + } + } +} + +private struct GlobalAppearanceKey: EnvironmentKey { + static let defaultValue = GlobalAppearance.companion.Default +} + +private struct TimelineAppearanceKey: EnvironmentKey { + static let defaultValue = TimelineAppearance.companion.Default +} + +private struct AiConfigKey: EnvironmentKey { + static let defaultValue = AppSettings.AiConfig.companion.default +} + +private struct TranslateConfigKey: EnvironmentKey { + static let defaultValue = AppSettings.TranslateConfig() +} + +private struct NetworkKindKey: EnvironmentKey { + static let defaultValue: NetworkKind = .cellular +} + +private struct IsMultipleColumnKey: EnvironmentKey { + static let defaultValue = false +} + +public extension EnvironmentValues { + var globalAppearance: GlobalAppearance { + get { self[GlobalAppearanceKey.self] } + set { self[GlobalAppearanceKey.self] = newValue } + } + + var timelineAppearance: TimelineAppearance { + get { self[TimelineAppearanceKey.self] } + set { self[TimelineAppearanceKey.self] = newValue } + } + + var aiConfig: AppSettings.AiConfig { + get { self[AiConfigKey.self] } + set { self[AiConfigKey.self] = newValue } + } + + var translateConfig: AppSettings.TranslateConfig { + get { self[TranslateConfigKey.self] } + set { self[TranslateConfigKey.self] = newValue } + } + + var networkKind: NetworkKind { + get { self[NetworkKindKey.self] } + set { self[NetworkKindKey.self] = newValue } + } + + var isMultipleColumn: Bool { + get { self[IsMultipleColumnKey.self] } + set { self[IsMultipleColumnKey.self] = newValue } + } +} diff --git a/iosApp/flare/UI/Component/ListCardView.swift b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/ListCardView.swift similarity index 67% rename from iosApp/flare/UI/Component/ListCardView.swift rename to appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/ListCardView.swift index 2c7c1ddd5c..593a127b1d 100644 --- a/iosApp/flare/UI/Component/ListCardView.swift +++ b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/ListCardView.swift @@ -1,21 +1,31 @@ import SwiftUI -struct ListCardView: View { +public struct ListCardView: View { @Environment(\.isMultipleColumn) private var isMultipleColumn - let index: Int - let totalCount: Int + private let index: Int + private let totalCount: Int @ViewBuilder - let content: () -> Content - let cornerRadius: CGFloat = 32 + private let content: () -> Content + private let cornerRadius: CGFloat = 32 - var body: some View { + public init( + index: Int, + totalCount: Int, + @ViewBuilder content: @escaping () -> Content + ) { + self.index = index + self.totalCount = totalCount + self.content = content + } + + public var body: some View { content() .background { UnevenRoundedRectangle( cornerRadii: cornerRadii, style: .continuous ) - .fill(Color(.secondarySystemGroupedBackground)) + .fill(Color.flareSecondarySystemGroupedBackground) } } @@ -38,21 +48,10 @@ struct ListCardView: View { } } -extension ListCardView { +public extension ListCardView { init( @ViewBuilder content: @escaping () -> Content ) { self.init(index: 0, totalCount: 1, content: content) } } - -private struct IsMultipleColumn: EnvironmentKey { - static let defaultValue: Bool = false -} - -extension EnvironmentValues { - var isMultipleColumn: Bool { - get { self[IsMultipleColumn.self] } - set { self[IsMultipleColumn.self] = newValue } - } -} diff --git a/iosApp/flare/UI/Component/ListEmptyView.swift b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/ListEmptyView.swift similarity index 71% rename from iosApp/flare/UI/Component/ListEmptyView.swift rename to appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/ListEmptyView.swift index 5e19d73ad4..ae56c0d97a 100644 --- a/iosApp/flare/UI/Component/ListEmptyView.swift +++ b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/ListEmptyView.swift @@ -1,7 +1,9 @@ import SwiftUI -struct ListEmptyView: View { - var body: some View { +public struct ListEmptyView: View { + public init() {} + + public var body: some View { ContentUnavailableView { Label { Text("list_empty_title") diff --git a/iosApp/flare/UI/Component/ListErrorView.swift b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/ListErrorView.swift similarity index 87% rename from iosApp/flare/UI/Component/ListErrorView.swift rename to appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/ListErrorView.swift index 1dad99f129..3b202ac1ec 100644 --- a/iosApp/flare/UI/Component/ListErrorView.swift +++ b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/ListErrorView.swift @@ -2,11 +2,17 @@ import SwiftUI import KotlinSharedUI import SwiftUIBackports -struct ListErrorView: View { - let error: KotlinThrowable +public struct ListErrorView: View { + private let error: KotlinThrowable @Environment(\.openURL) private var openURL - let onRetry: () -> Void - var body: some View { + private let onRetry: () -> Void + + public init(error: KotlinThrowable, onRetry: @escaping () -> Void) { + self.error = error + self.onRetry = onRetry + } + + public var body: some View { VStack(spacing: 8) { if let expiredError = error as? LoginExpiredException { Image(systemName: "person.badge.shield.exclamationmark") @@ -25,7 +31,7 @@ struct ListErrorView: View { } .backport .glassProminentButtonStyle() - } else if let requireReLogin = error as? RequireReLoginException { + } else if error is RequireReLoginException { Image(systemName: "person.badge.shield.exclamationmark") .resizable() .scaledToFit() diff --git a/iosApp/flare/UI/Component/NetworkImage.swift b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/NetworkImage.swift similarity index 93% rename from iosApp/flare/UI/Component/NetworkImage.swift rename to appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/NetworkImage.swift index 214cebfdb4..d5dab165e0 100644 --- a/iosApp/flare/UI/Component/NetworkImage.swift +++ b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/NetworkImage.swift @@ -1,11 +1,12 @@ import SwiftUI import Kingfisher -struct NetworkImage: View { - let data: URL? - let placeholder: URL? - let customHeader: [String: String]? - var body: some View { +public struct NetworkImage: View { + private let data: URL? + private let placeholder: URL? + private let customHeader: [String: String]? + + public var body: some View { if data?.absoluteString.hasSuffix(".gif") == true { KFAnimatedImage(data) .fade(duration: 0.25) @@ -65,7 +66,7 @@ struct NetworkImage: View { } } -extension NetworkImage { +public extension NetworkImage { init(data: String?, customHeader: [String: String]? = nil) { self.init(data: data.flatMap(URL.init(string:)), placeholder: nil, customHeader: customHeader) } diff --git a/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/PagingView.swift b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/PagingView.swift new file mode 100644 index 0000000000..9a566eb034 --- /dev/null +++ b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/PagingView.swift @@ -0,0 +1,172 @@ +import SwiftUI +import KotlinSharedUI + +public struct PagingView< + T: AnyObject, + EmptyContent: View, + ErrorContent: View, + LoadingContent: View, + SuccessContent: View +>: View { + private let data: PagingState + private let emptyContent: () -> EmptyContent + private let errorContent: (KotlinThrowable, @escaping () -> Void) -> ErrorContent + private let loadingContent: (Int, Int) -> LoadingContent + private let loadingCount = 5 + private let maxCount: Int? + private let successContent: (T, Int, Int) -> SuccessContent + + public init( + data: PagingState, + maxCount: Int? = nil, + @ViewBuilder emptyContent: @escaping () -> EmptyContent, + @ViewBuilder errorContent: @escaping (KotlinThrowable, @escaping () -> Void) -> ErrorContent, + @ViewBuilder loadingContent: @escaping (Int, Int) -> LoadingContent, + @ViewBuilder successContent: @escaping (T, Int, Int) -> SuccessContent + ) { + self.data = data + self.maxCount = maxCount + self.emptyContent = emptyContent + self.errorContent = errorContent + self.loadingContent = loadingContent + self.successContent = successContent + } + + public var body: some View { + switch onEnum(of: data) { + case .empty: + emptyContent() + case .error(let error): + errorContent(error.error) { + _ = error.onRetry() + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) + case .loading: + let visibleLoadingCount = cappedCount(loadingCount) + ForEach(0.. Int { + guard let maxCount else { + return count + } + return min(count, max(0, maxCount)) + } + + private func isMaxCountReached(_ count: Int) -> Bool { + guard let maxCount else { + return false + } + return count >= max(0, maxCount) + } +} + +public extension PagingView { + init( + data: PagingState, + maxCount: Int? = nil, + @ViewBuilder successContent: @escaping (T) -> SuccessContent, + @ViewBuilder loadingContent: @escaping () -> LoadingContent + ) where ErrorContent == ListErrorView, EmptyContent == ListEmptyView { + self.init( + data: data, + maxCount: maxCount, + emptyContent: { ListEmptyView() }, + errorContent: { error, retry in + ListErrorView(error: error) { + retry() + } + }, + loadingContent: { _, _ in + loadingContent() + }, + successContent: { item, _, _ in + successContent(item) + } + ) + } + + init( + data: PagingState, + maxCount: Int? = nil, + @ViewBuilder successContent: @escaping (T) -> SuccessContent, + @ViewBuilder loadingContent: @escaping () -> LoadingContent, + @ViewBuilder errorContent: @escaping (KotlinThrowable, @escaping () -> Void) -> ErrorContent + ) where EmptyContent == ListEmptyView { + self.init( + data: data, + maxCount: maxCount, + emptyContent: { ListEmptyView() }, + errorContent: { error, retry in + errorContent(error, retry) + }, + loadingContent: { _, _ in + loadingContent() + }, + successContent: { item, _, _ in + successContent(item) + } + ) + } + + init( + data: PagingState, + maxCount: Int? = nil, + @ViewBuilder successContent: @escaping (T) -> SuccessContent, + @ViewBuilder loadingContent: @escaping () -> LoadingContent, + @ViewBuilder errorContent: @escaping (KotlinThrowable, @escaping () -> Void) -> ErrorContent, + @ViewBuilder emptyContent: @escaping () -> EmptyContent + ) { + self.init( + data: data, + maxCount: maxCount, + emptyContent: { emptyContent() }, + errorContent: { error, retry in + errorContent(error, retry) + }, + loadingContent: { _, _ in + loadingContent() + }, + successContent: { item, _, _ in + successContent(item) + } + ) + } +} diff --git a/iosApp/flare/UI/Component/CommonProfileHeader.swift b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/ProfileHeader.swift similarity index 52% rename from iosApp/flare/UI/Component/CommonProfileHeader.swift rename to appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/ProfileHeader.swift index 926f7ae570..17a0c96a44 100644 --- a/iosApp/flare/UI/Component/CommonProfileHeader.swift +++ b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/ProfileHeader.swift @@ -1,10 +1,12 @@ -import SwiftUI +import AppleFontAwesome +import FlareAppleCore import KotlinSharedUI +import SwiftUI import SwiftUIBackports -enum CommonProfileHeaderConstants { - static let headerHeight: CGFloat = 200 - static let avatarSize: CGFloat = 96 +public enum CommonProfileHeaderConstants { + public static let headerHeight: CGFloat = 200 + public static let avatarSize: CGFloat = 96 } private extension FollowButtonState { @@ -24,26 +26,53 @@ private extension FollowButtonState { } } -struct CommonProfileHeader: View { +public struct CommonProfileHeader: View { @Environment(\.timelineAppearance.timelineDisplayMode) private var timelineDisplayMode - @Environment(\.horizontalSizeClass) private var horizontalSizeClass @Environment(\.openURL) private var openURL - let user: UiProfile - let relation: UiState - let followButtonState: UiState - let isMe: UiState - let onFollowClick: (FollowButtonState) -> Void - let onFollowingClick: () -> Void - let onFansClick: () -> Void + #if os(iOS) + @Environment(\.horizontalSizeClass) private var horizontalSizeClass + #endif + + private let user: UiProfile + private let relation: UiState + private let followButtonState: UiState + private let isMe: UiState + private let onFollowClick: (FollowButtonState) -> Void + private let onFollowingClick: () -> Void + private let onFansClick: () -> Void - var body: some View { + public init( + user: UiProfile, + relation: UiState, + followButtonState: UiState, + isMe: UiState, + onFollowClick: @escaping (FollowButtonState) -> Void, + onFollowingClick: @escaping () -> Void, + onFansClick: @escaping () -> Void + ) { + self.user = user + self.relation = relation + self.followButtonState = followButtonState + self.isMe = isMe + self.onFollowClick = onFollowClick + self.onFollowingClick = onFollowingClick + self.onFansClick = onFansClick + } + + public var body: some View { ZStack(alignment: .top) { if let banner = user.banner { Color.clear.overlay { NetworkImage(data: banner.url, customHeader: banner.customHeaders) .frame(height: CommonProfileHeaderConstants.headerHeight) .onTapGesture { - if let url = URL(string: DeeplinkRoute.Media.MediaImage(uri: banner.url, previewUrl: nil, customHeaders: banner.customHeaders).toUri()) { + if let url = URL( + string: DeeplinkRoute.Media.MediaImage( + uri: banner.url, + previewUrl: nil, + customHeaders: banner.customHeaders + ).toUri() + ) { openURL.callAsFunction(url) } } @@ -62,13 +91,19 @@ struct CommonProfileHeader: View { Spacer() .frame( height: CommonProfileHeaderConstants.headerHeight - - CommonProfileHeaderConstants.avatarSize / 2 + CommonProfileHeaderConstants.avatarSize / 2 ) AvatarView(data: user.avatar?.url, customHeader: user.avatar?.customHeaders) .frame(width: CommonProfileHeaderConstants.avatarSize, height: CommonProfileHeaderConstants.avatarSize) .onTapGesture { if let avatar = user.avatar, - let url = URL(string: DeeplinkRoute.Media.MediaImage(uri: avatar.url, previewUrl: nil, customHeaders: avatar.customHeaders).toUri()) { + let url = URL( + string: DeeplinkRoute.Media.MediaImage( + uri: avatar.url, + previewUrl: nil, + customHeaders: avatar.customHeaders + ).toUri() + ) { openURL.callAsFunction(url) } } @@ -95,19 +130,21 @@ struct CommonProfileHeader: View { } } .animation(.spring(response: 0.25, dampingFraction: 0.86), value: buttonState.data.id) - case .loading: Button(action: {}, label: { - Text("#loading") - }) - .backport - .glassProminentButtonStyle() - .redacted(reason: .placeholder) - case .error: EmptyView() + case .loading: + Button(action: {}, label: { + Text("#loading") + }) + .backport + .glassProminentButtonStyle() + .redacted(reason: .placeholder) + case .error: + EmptyView() } } } } - if horizontalSizeClass == .compact, timelineDisplayMode != .plain { + if shouldWrapContentInCard { ListCardView { content .padding() @@ -120,6 +157,14 @@ struct CommonProfileHeader: View { } } + private var shouldWrapContentInCard: Bool { + #if os(iOS) + horizontalSizeClass == .compact && timelineDisplayMode != .plain + #else + false + #endif + } + @ViewBuilder private func followButton(state: FollowButtonState, action: @escaping () -> Void) -> some View { switch onEnum(of: state) { @@ -143,8 +188,8 @@ struct CommonProfileHeader: View { .glassProminentButtonStyle() } } - - var content: some View { + + private var content: some View { VStack( alignment: .leading, spacing: 8 @@ -161,10 +206,10 @@ struct CommonProfileHeader: View { ForEach(0.. Void - let onFansClick: () -> Void - var body: some View { +public struct ProfileHeader: View { + private let user: UiState + private let relation: UiState + private let followButtonState: UiState + private let isMe: UiState + private let onFollowClick: (UiProfile, FollowButtonState) -> Void + private let onFollowingClick: (MicroBlogKey) -> Void + private let onFansClick: (MicroBlogKey) -> Void + + public init( + user: UiState, + relation: UiState, + followButtonState: UiState, + isMe: UiState, + onFollowClick: @escaping (UiProfile, FollowButtonState) -> Void, + onFollowingClick: @escaping (MicroBlogKey) -> Void, + onFansClick: @escaping (MicroBlogKey) -> Void + ) { + self.user = user + self.relation = relation + self.followButtonState = followButtonState + self.isMe = isMe + self.onFollowClick = onFollowClick + self.onFollowingClick = onFollowingClick + self.onFansClick = onFansClick + } + + public var body: some View { + switch onEnum(of: user) { + case .error: + Text("error") + case .loading: + CommonProfileHeader( + user: createSampleUser(), + relation: relation, + followButtonState: followButtonState, + isMe: isMe, + onFollowClick: { _ in }, + onFollowingClick: {}, + onFansClick: {} + ) + .redacted(reason: .placeholder) + case .success(let data): + ProfileHeaderSuccess( + user: data.data, + relation: relation, + followButtonState: followButtonState, + isMe: isMe, + onFollowClick: { followButtonState in onFollowClick(data.data, followButtonState) }, + onFollowingClick: onFollowingClick, + onFansClick: onFansClick + ) + } + } +} + +public struct ProfileHeaderSuccess: View { + private let user: UiProfile + private let relation: UiState + private let followButtonState: UiState + private let isMe: UiState + private let onFollowClick: (FollowButtonState) -> Void + private let onFollowingClick: (MicroBlogKey) -> Void + private let onFansClick: (MicroBlogKey) -> Void + + public init( + user: UiProfile, + relation: UiState, + followButtonState: UiState, + isMe: UiState, + onFollowClick: @escaping (FollowButtonState) -> Void, + onFollowingClick: @escaping (MicroBlogKey) -> Void, + onFansClick: @escaping (MicroBlogKey) -> Void + ) { + self.user = user + self.relation = relation + self.followButtonState = followButtonState + self.isMe = isMe + self.onFollowClick = onFollowClick + self.onFollowingClick = onFollowingClick + self.onFansClick = onFansClick + } + + public var body: some View { + CommonProfileHeader( + user: user, + relation: relation, + followButtonState: followButtonState, + isMe: isMe, + onFollowClick: onFollowClick, + onFollowingClick: { + onFollowingClick(user.key) + }, + onFansClick: { + onFansClick(user.key) + } + ) + } +} + +public struct MatrixView: View { + private let followCount: String + private let fansCount: String + private let onFollowingClick: () -> Void + private let onFansClick: () -> Void + + public init( + followCount: String, + fansCount: String, + onFollowingClick: @escaping () -> Void, + onFansClick: @escaping () -> Void + ) { + self.followCount = followCount + self.fansCount = fansCount + self.onFollowingClick = onFollowingClick + self.onFansClick = onFansClick + } + + public var body: some View { HStack { HStack { Text(followCount) @@ -224,9 +382,14 @@ struct MatrixView: View { } } -struct IconFieldView: View { - let data: UiProfileBottomContentIconify - var body: some View { +public struct IconFieldView: View { + private let data: UiProfileBottomContentIconify + + public init(data: UiProfileBottomContentIconify) { + self.data = data + } + + public var body: some View { VStack(alignment: .leading, spacing: 8) { ForEach(Array(data.items.keys), id: \.name) { key in let value = data.items[key] @@ -239,9 +402,9 @@ struct IconFieldView: View { }, icon: { switch key { - case .location: Image("fa-location-dot") - case .url: Image("fa-globe") - case .verify: Image("fa-circle-check") + case .location: Image(fontAwesome: .locationDot) + case .url: Image(fontAwesome: .globe) + case .verify: Image(fontAwesome: .circleCheck) } } ) @@ -250,9 +413,14 @@ struct IconFieldView: View { } } -struct FieldsView: View { - let fields: [String: UiRichText] - var body: some View { +public struct FieldsView: View { + private let fields: [String: UiRichText] + + public init(fields: [String: UiRichText]) { + self.fields = fields + } + + public var body: some View { if fields.count > 0 { VStack(alignment: .leading, spacing: 8) { let keys = fields.map { @@ -276,7 +444,7 @@ struct FieldsView: View { .padding(.horizontal) } .padding(.vertical) - .background(Color(.secondarySystemBackground)) + .background(Color.flareSecondarySystemBackground) .clipShape(RoundedRectangle(cornerRadius: 8)) } else { EmptyView() diff --git a/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/ProfileTabs.swift b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/ProfileTabs.swift new file mode 100644 index 0000000000..f48e867bad --- /dev/null +++ b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/ProfileTabs.swift @@ -0,0 +1,46 @@ +import KotlinSharedUI +import SwiftUI +import FlareAppleCore + +public struct ProfileTabPicker: View { + private let tabs: [ProfileState.Tab] + @Binding private var selectedTab: Int + + public init(tabs: [ProfileState.Tab], selectedTab: Binding) { + self.tabs = tabs + self._selectedTab = selectedTab + } + + public var body: some View { + Picker(selection: $selectedTab) { + ForEach(0.. String { + tab.name.text +} + +public func profileTimelineID(for tab: ProfileState.Tab) -> String { + switch onEnum(of: tab) { + case .timeline: + "Timeline_\(tab.name.name)" + case .media: + "Media_\(tab.name.name)" + } +} + +public func profileTimelinePresenter(for tab: ProfileState.Tab) -> TimelinePresenter { + switch onEnum(of: tab) { + case .timeline(let tab): + tab.presenter + case .media(let tab): + tab.presenter.getMediaTimelinePresenter() + } +} diff --git a/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/QRCodeView.swift b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/QRCodeView.swift new file mode 100644 index 0000000000..a7257724c0 --- /dev/null +++ b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/QRCodeView.swift @@ -0,0 +1,38 @@ +import CoreImage.CIFilterBuiltins +import SwiftUI + +public struct QRCodeView: View { + private let text: String + + public init(text: String) { + self.text = text + } + + public var body: some View { + if let image = makeQRCode(from: text) { + Image(decorative: image, scale: 1, orientation: .up) + .interpolation(.none) + .resizable() + .scaledToFit() + .padding(12) + } else { + Image(systemName: "qrcode") + .resizable() + .scaledToFit() + .padding(40) + .foregroundStyle(.secondary) + } + } + + private func makeQRCode(from text: String) -> CGImage? { + let filter = CIFilter.qrCodeGenerator() + filter.message = Data(text.utf8) + + guard let outputImage = filter.outputImage else { + return nil + } + + let scaledImage = outputImage.transformed(by: CGAffineTransform(scaleX: 10, y: 10)) + return CIContext().createCGImage(scaledImage, from: scaledImage.extent) + } +} diff --git a/iosApp/flare/UI/Component/RichText.swift b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/RichText.swift similarity index 65% rename from iosApp/flare/UI/Component/RichText.swift rename to appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/RichText.swift index 6bc7e56d25..466937236c 100644 --- a/iosApp/flare/UI/Component/RichText.swift +++ b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/RichText.swift @@ -1,15 +1,30 @@ import SwiftUI import KotlinSharedUI import Kingfisher +import FlareAppleCore -struct RichText: View { - let text: UiRichText +#if canImport(UIKit) +import UIKit + +private typealias PlatformImage = UIImage +#elseif canImport(AppKit) +import AppKit + +private typealias PlatformImage = NSImage +#endif + +public struct RichText: View { + private let text: UiRichText @State private var images: [String: Image] = [:] - @ScaledMetric(relativeTo: .body) var imageSize = 17 + @ScaledMetric(relativeTo: .body) private var imageSize = 17 @ScaledMetric(relativeTo: .body) private var quoteBarWidth = 3 - @Environment(\.openURL) var openURL + @Environment(\.openURL) private var openURL - var body: some View { + public init(text: UiRichText) { + self.text = text + } + + public var body: some View { VStack(alignment: .leading, spacing: 8) { ForEach(Array(contents.enumerated()), id: \.offset) { _, content in switch content { @@ -36,35 +51,22 @@ struct RichText: View { .task(id: text.raw) { let urls = text.imageUrls let targetHeight = imageSize - await withTaskGroup(of: (String, Image?).self) { group in - for urlString in urls { - group.addTask { - guard let url = URL(string: urlString) else { return (urlString, nil) } - do { - let result = try await KingfisherManager.shared.retrieveImage(with: url) - let uiImage = result.image - if let resized = await uiImage.resize(height: targetHeight) { - return (urlString, Image(uiImage: resized)) - } else { - return (urlString, Image(uiImage: uiImage)) - } - } catch { - return (urlString, nil) - } - } - } - - for await (urlString, image) in group { - if let image = image { - images[urlString] = image - } + for urlString in urls { + guard let url = URL(string: urlString) else { continue } + do { + let result = try await KingfisherManager.shared.retrieveImage(with: url) + let platformImage = result.image + let resized = platformImage.resize(height: targetHeight) ?? platformImage + images[urlString] = Image(platformImage: resized) + } catch { + continue } } } } private var contents: [PlatformTextContent] { - (text.platformText as? NSArray)?.compactMap { $0 as? PlatformTextContent } ?? [] + text.platformText.compactMap { $0 as? PlatformTextContent } } @ViewBuilder @@ -118,7 +120,18 @@ struct RichText: View { } } -extension UIImage { +private extension Image { + init(platformImage: PlatformImage) { + #if canImport(UIKit) + self.init(uiImage: platformImage) + #elseif canImport(AppKit) + self.init(nsImage: platformImage) + #endif + } +} + +#if canImport(UIKit) +private extension UIImage { func resize(height: CGFloat) -> UIImage? { let heightRatio = height / size.height let width = size.width * heightRatio @@ -133,3 +146,25 @@ extension UIImage { return resizedImage } } +#elseif canImport(AppKit) +private extension NSImage { + func resize(height: CGFloat) -> NSImage? { + guard size.height > 0 else { return self } + let heightRatio = height / size.height + let width = size.width * heightRatio + let resizedSize = CGSize(width: width, height: height) + let resizedImage = NSImage(size: resizedSize) + + resizedImage.lockFocus() + draw( + in: CGRect(origin: .zero, size: resizedSize), + from: CGRect(origin: .zero, size: size), + operation: .copy, + fraction: 1.0 + ) + resizedImage.unlockFocus() + + return resizedImage + } +} +#endif diff --git a/iosApp/flare/UI/Component/AdaptiveGrid.swift b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelineItem/AdaptiveGrid.swift similarity index 73% rename from iosApp/flare/UI/Component/AdaptiveGrid.swift rename to appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelineItem/AdaptiveGrid.swift index f3fb0b24c5..27725d4aab 100644 --- a/iosApp/flare/UI/Component/AdaptiveGrid.swift +++ b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelineItem/AdaptiveGrid.swift @@ -1,6 +1,6 @@ import SwiftUI -struct AdaptiveGrid: Layout { +public struct AdaptiveGrid: Layout { public let singleFollowsImageAspect: Bool public let singleViewAspectRatio: CGFloat? @@ -19,9 +19,14 @@ struct AdaptiveGrid: Layout { self.singleViewAspectRatio = singleViewAspectRatio } - public struct Cache {} + public struct Cache { + var singleAspectRatio: CGFloat? + } + public func makeCache(subviews: Subviews) -> Cache { Cache() } - public func updateCache(_ cache: inout Cache, subviews: Subviews) {} + public func updateCache(_ cache: inout Cache, subviews: Subviews) { + cache.singleAspectRatio = nil + } public func sizeThatFits( proposal: ProposedViewSize, @@ -36,7 +41,7 @@ struct AdaptiveGrid: Layout { switch count { case 1: - let ratio = aspectForSingle(subviews: subviews) + let ratio = aspectForSingle(subviews: subviews, cache: &cache) if let height = proposal.height, proposal.width == nil { width = height * ratio } return CGSize(width: width, height: width / ratio) @@ -67,13 +72,9 @@ struct AdaptiveGrid: Layout { let count = subviews.count guard count > 0 else { return } - let size = sizeThatFits( - proposal: ProposedViewSize(width: bounds.width, height: bounds.height), - subviews: subviews, - cache: &cache - ) - let width = size.width - let height = size.height + let width = max(0, bounds.width) + let singleAspectRatio = aspectForSinglePlacement(cache: cache) + let height = heightForLayout(width: width, count: count, singleAspectRatio: singleAspectRatio) let spacing = spacing let origin = CGPoint(x: bounds.minX, y: bounds.minY) @@ -88,8 +89,7 @@ struct AdaptiveGrid: Layout { switch count { case 1: - let ratio = aspectForSingle(subviews: subviews) - place(0, x: 0, y: 0, width: width, height: width / ratio) + place(0, x: 0, y: 0, width: width, height: width / singleAspectRatio) case 2: let cellW = (width - spacing) / 2 @@ -116,7 +116,7 @@ struct AdaptiveGrid: Layout { let fullRows = count / cols let rem = count % cols - let columnWidth = (width - CGFloat(cols - 1) * spacing) / CGFloat(cols) // 行高 + let columnWidth = (width - CGFloat(cols - 1) * spacing) / CGFloat(cols) var idx = 0 var y: CGFloat = 0 @@ -141,17 +141,53 @@ struct AdaptiveGrid: Layout { } } - private func aspectForSingle(subviews: Subviews) -> CGFloat { + private func aspectForSingle(subviews: Subviews, cache: inout Cache) -> CGFloat { + if let cached = cache.singleAspectRatio { + return cached + } + + let ratio: CGFloat if singleFollowsImageAspect { - if let ratio = singleViewAspectRatio { - return max(9.0/21.0, ratio) + if let providedRatio = singleViewAspectRatio { + ratio = max(9.0 / 21.0, providedRatio) } else { let ideal = subviews[0].sizeThatFits(.unspecified) - if ideal.width > 0, ideal.height > 0 { return max(0.01, ideal.width / ideal.height) } - return 1 + if ideal.width > 0, ideal.height > 0 { + ratio = max(0.01, ideal.width / ideal.height) + } else { + ratio = 1 + } } } else { - return 16.0 / 9.0 + ratio = 16.0 / 9.0 + } + + cache.singleAspectRatio = ratio + return ratio + } + + private func aspectForSinglePlacement(cache: Cache) -> CGFloat { + if let cached = cache.singleAspectRatio { + return cached + } + if singleFollowsImageAspect { + if let ratio = singleViewAspectRatio { + return max(9.0 / 21.0, ratio) + } + return 1 + } + return 16.0 / 9.0 + } + + private func heightForLayout(width: CGFloat, count: Int, singleAspectRatio: CGFloat) -> CGFloat { + switch count { + case 1: + return width / singleAspectRatio + case 2, 3, 4: + return width / (16.0 / 9.0) + default: + let cols = min(maxColumns, 3) + return heightForGridFillLastRow(width: width, count: count, cols: cols, spacing: spacing) } } diff --git a/iosApp/flare/UI/Component/AvatarView.swift b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelineItem/AvatarView.swift similarity index 62% rename from iosApp/flare/UI/Component/AvatarView.swift rename to appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelineItem/AvatarView.swift index 35350b2f04..7453f5cdcd 100644 --- a/iosApp/flare/UI/Component/AvatarView.swift +++ b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelineItem/AvatarView.swift @@ -1,17 +1,17 @@ import SwiftUI import KotlinSharedUI -struct AvatarView: View { +public struct AvatarView: View { @Environment(\.timelineAppearance.avatarShape) private var avatarShape - let data: String? - let customHeader: [String: String]? + private let data: String? + private let customHeader: [String: String]? - init(data: String?, customHeader: [String: String]? = nil) { + public init(data: String?, customHeader: [String: String]? = nil) { self.data = data self.customHeader = customHeader } - var body: some View { + public var body: some View { NetworkImage(data: data, customHeader: customHeader) .clipShape(avatarShape == .circle ? AnyShape(.circle) : AnyShape(.rect(cornerRadius: 8))) } diff --git a/iosApp/flare/UI/Component/DateTimeText.swift b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelineItem/DateTimeText.swift similarity index 61% rename from iosApp/flare/UI/Component/DateTimeText.swift rename to appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelineItem/DateTimeText.swift index c14e4a3f56..5f29fc8967 100644 --- a/iosApp/flare/UI/Component/DateTimeText.swift +++ b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelineItem/DateTimeText.swift @@ -1,12 +1,17 @@ import SwiftUI import KotlinSharedUI -struct DateTimeText: View { +public struct DateTimeText: View { @Environment(\.timelineAppearance.absoluteTimestamp) private var absoluteTimestamp - let data: UiDateTime - let fullTime: Bool + private let data: UiDateTime + private let fullTime: Bool - var body: some View { + public init(data: UiDateTime, fullTime: Bool) { + self.data = data + self.fullTime = fullTime + } + + public var body: some View { if fullTime { Text(data.full) } else if data.shouldShowFull { @@ -19,9 +24,8 @@ struct DateTimeText: View { } } -extension DateTimeText { +public extension DateTimeText { init(data: UiDateTime) { - self.data = data - self.fullTime = false + self.init(data: data, fullTime: false) } } diff --git a/iosApp/flare/UI/Component/Status/FeedView.swift b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelineItem/FeedView.swift similarity index 99% rename from iosApp/flare/UI/Component/Status/FeedView.swift rename to appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelineItem/FeedView.swift index 89cef08dbb..e66ba3208d 100644 --- a/iosApp/flare/UI/Component/Status/FeedView.swift +++ b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelineItem/FeedView.swift @@ -1,5 +1,6 @@ import SwiftUI import KotlinSharedUI +import FlareAppleCore struct FeedView: View { @Environment(\.openURL) private var openURL diff --git a/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelineItem/MediaView.swift b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelineItem/MediaView.swift new file mode 100644 index 0000000000..15b6a21051 --- /dev/null +++ b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelineItem/MediaView.swift @@ -0,0 +1,52 @@ +import SwiftUI +import KotlinSharedUI +import AppleFontAwesome + +public struct MediaView: View { + public let data: UiMedia + + public init(data: UiMedia) { + self.data = data + } + + public var body: some View { + ZStack { + switch onEnum(of: data) { + case .image(let image): + Color.gray + .overlay { + NetworkImage(data: image.previewUrl, customHeader: image.customHeaders) + .allowsHitTesting(false) + } + .clipped() + case .video(let video): + Color.gray + .overlay { + NetworkImage(data: video.thumbnailUrl, customHeader: video.customHeaders) + .allowsHitTesting(false) + } + .overlay(alignment: .bottomLeading) { + Image(fontAwesome: .circlePlay) + .foregroundStyle(.white) + .padding(8) + .background(.black, in: RoundedRectangle(cornerRadius: 16, style: .continuous)) + .padding() + } + .clipped() + case .gif(let gif): + Color.gray + .overlay { + NetworkImage(data: gif.url, customHeader: gif.customHeaders) + .allowsHitTesting(false) + } + .clipped() + case .audio: + Color.gray + .overlay { + Image(systemName: "waveform") + .foregroundStyle(.white) + } + } + } + } +} diff --git a/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelineItem/StatusActionView.swift b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelineItem/StatusActionView.swift new file mode 100644 index 0000000000..dda38d3372 --- /dev/null +++ b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelineItem/StatusActionView.swift @@ -0,0 +1,351 @@ +import SwiftUI +import KotlinSharedUI +import SwiftUIBackports +import FlareAppleCore + +#if canImport(UIKit) +import UIKit +#endif + +// MARK: - Top-level container +// Hoists @ScaledMetric, @Environment reads to a single place +// instead of duplicating them in every child view instance. + +public struct StatusActionsView: View { + @Environment(\.timelineAppearance.postActionStyle) private var postActionStyle + @Environment(\.timelineAppearance.showNumbers) private var showNumbers + @Environment(\.timelineAppearance.postActionLayout) private var postActionLayout + @Environment(\.openURL) private var openURL + @ScaledMetric(relativeTo: .footnote) private var fontSize = 13 + private let data: [ActionMenu] + private let useText: Bool + private let allowSpacer: Bool + private let applyPostActionLayout: Bool + + public init( + data: [ActionMenu], + useText: Bool, + allowSpacer: Bool = true, + applyPostActionLayout: Bool = true + ) { + self.data = data + self.useText = useText + self.allowSpacer = allowSpacer + self.applyPostActionLayout = applyPostActionLayout + } + + public var body: some View { + let actions = resolvedData + if useText { + ForEach(0.. some View { + if isEnabled { + #if os(macOS) + content + .padding(.horizontal, horizontalPadding) + .padding(.vertical, verticalPadding) + .contentShape(Rectangle()) + .background { + RoundedRectangle(cornerRadius: 6, style: .continuous) + .fill(Color.primary.opacity(isHovered ? 0.08 : 0)) + } + .animation(.easeOut(duration: 0.12), value: isHovered) + .onHover { hovering in + isHovered = hovering + } + #else + content + .padding(.horizontal, horizontalPadding) + .padding(.vertical, verticalPadding) + .contentShape(Rectangle()) + #endif + } else { + content + } + } +} + +private extension View { + @ViewBuilder + func labelIconToTitleSpacingIfAvailable(_ spacing: CGFloat) -> some View { + self.backport.flareLabelIconToTitleSpacing(spacing) + } + + @ViewBuilder + func optionalForegroundStyle(_ color: Color?) -> some View { + if let color { + self.foregroundStyle(color) + } else { + self + } + } + + @ViewBuilder + func statusActionHitTarget(isEnabled: Bool) -> some View { + modifier(StatusActionHitTargetModifier(isEnabled: isEnabled)) + } +} + +public extension ActionMenu.ItemColor { + var swiftColor: Color? { + switch self { + case .red: return .red + case .contentColor: return .primary + case .primaryColor: return .accentColor + } + } + + var role: ButtonRole? { + switch self { + case .red: + .destructive + case .primaryColor: + #if os(iOS) + if #available(iOS 26.0, *) { + .confirm + } else { + nil + } + #else + nil + #endif + default: + nil + } + } +} + +public struct StatusActionIcon: View { + private let icon: UiIcon? + + public init(icon: UiIcon?) { + self.icon = icon + } + + public var body: some View { + if let icon = icon { + icon.image + } + } +} + +private func castActionMenus(_ value: Any) -> [ActionMenu] { + if let actions = value as? [ActionMenu] { + return actions + } + if let actions = value as? NSArray { + return actions.cast(ActionMenu.self) + } + return [] +} diff --git a/iosApp/flare/UI/Component/Status/StatusCardView.swift b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelineItem/StatusCardView.swift similarity index 96% rename from iosApp/flare/UI/Component/Status/StatusCardView.swift rename to appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelineItem/StatusCardView.swift index f7af406d2c..4f73f77030 100644 --- a/iosApp/flare/UI/Component/Status/StatusCardView.swift +++ b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelineItem/StatusCardView.swift @@ -1,5 +1,6 @@ import SwiftUI import KotlinSharedUI +import FlareAppleCore struct StatusCardView: View { @Environment(\.openURL) private var openURL @@ -51,7 +52,7 @@ struct StatusCardView: View { .clipShape(.rect(cornerRadius: cornerRadius)) .overlay( RoundedRectangle(cornerRadius: cornerRadius) - .stroke(Color(.separator), lineWidth: 1) + .stroke(Color.flareSeparator, lineWidth: 1) ) .onTapGesture { if let url = URL(string: data.url) { @@ -61,11 +62,11 @@ struct StatusCardView: View { } } - struct StatusCompatCardView: View { @Environment(\.openURL) private var openURL let data: UiCard let cornerRadius: CGFloat + var body: some View { HStack( spacing: 8 @@ -110,7 +111,7 @@ struct StatusCompatCardView: View { .clipShape(.rect(cornerRadius: cornerRadius)) .overlay( RoundedRectangle(cornerRadius: cornerRadius) - .stroke(Color(.separator), lineWidth: 1) + .stroke(Color.flareSeparator, lineWidth: 1) ) .onTapGesture { if let url = URL(string: data.url) { diff --git a/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelineItem/StatusMediaVideoView.swift b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelineItem/StatusMediaVideoView.swift new file mode 100644 index 0000000000..52b84b990a --- /dev/null +++ b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelineItem/StatusMediaVideoView.swift @@ -0,0 +1,411 @@ +import SwiftUI +import KotlinSharedUI +import AVFoundation + +#if canImport(UIKit) +import UIKit +#endif + +#if canImport(VideoPlayer) +import VideoPlayer +#endif + +public enum VideoState { + case idle + case loading + case playing(Double) + case paused(Double) + case error(any Error) +} + +public struct StatusMediaVideoView: View { + @Binding private var play: Bool + @Binding private var videoState: VideoState + @Binding private var time: CMTime + @Binding private var playbackRate: Float + private let data: UiMediaVideo + #if canImport(UIKit) + @State private var playbackResumeTask: Task? + @State private var fastPlaybackWasPlaying: Bool = false + @State private var isFastPlaybackActive: Bool = false + @State private var seekFeedback: SeekFeedback? + @State private var seekFeedbackOpacity: Double = 0 + @State private var seekFeedbackTask: Task? + private let seekInterval: Double = 5 + private let normalPlaybackRate: Float = 1 + private let fastPlaybackRate: Float = 2 + #endif + + public init( + data: UiMediaVideo, + play: Binding, + videoState: Binding, + time: Binding, + playbackRate: Binding = .constant(1) + ) { + self.data = data + self._play = play + self._videoState = videoState + self._time = time + self._playbackRate = playbackRate + } + + public var body: some View { + Color.clear + .overlay { + if case .idle = videoState { + NetworkImage(data: data.thumbnailUrl, customHeader: data.customHeaders) + .scaledToFit() + .allowsHitTesting(false) + } else { + EmptyView() + } + } + .clipped() + .overlay { + player + } + .overlay { + gestureOverlay + } + .overlay { + seekFeedbackOverlay + } + .onDisappear { + handleDisappear() + } + } + + @ViewBuilder + private var player: some View { + #if canImport(VideoPlayer) + if let videoURL = URL(string: data.url) { + VideoPlayer(url: videoURL, play: $play, time: $time) + .mute(false) + .autoReplay(true) + .speedRate(playbackRate) + .onStateChanged { state in + switch state { + case .playing(let duration): + videoState = .playing(duration) + case .loading: + videoState = .loading + case .paused: + if case .playing(let duration) = videoState { + videoState = .paused(duration) + } else if case .paused(let duration) = videoState { + videoState = .paused(duration) + } else { + videoState = .idle + } + case .error(let error): + videoState = .error(error) + } + } + .contentMode(.scaleAspectFit) + .allowsHitTesting(false) + } + #else + EmptyView() + #endif + } + + @ViewBuilder + private var gestureOverlay: some View { + #if canImport(UIKit) + VideoGestureOverlay( + onDoubleTap: { x, width in + if x < width / 2 { + seek(by: -seekInterval) + showSeekFeedback(.backward) + } else { + seek(by: seekInterval) + showSeekFeedback(.forward) + } + }, + onLongPressChanged: { pressing in + if pressing { + beginFastPlayback() + } else { + endFastPlayback() + } + } + ) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .accessibilityHidden(true) + #else + EmptyView() + #endif + } + + @ViewBuilder + private var seekFeedbackOverlay: some View { + #if canImport(UIKit) + if let seekFeedback { + HStack { + if seekFeedback == .forward { + Spacer() + } + Image(systemName: seekFeedback.iconName) + .font(.system(size: 44, weight: .semibold)) + .foregroundStyle(.white) + .padding(20) + .background(.black.opacity(0.55), in: .circle) + if seekFeedback == .backward { + Spacer() + } + } + .padding(.horizontal, 56) + .opacity(seekFeedbackOpacity) + } + #else + EmptyView() + #endif + } + + private func handleDisappear() { + #if canImport(UIKit) + endFastPlayback() + seekFeedbackTask?.cancel() + #endif + } + + #if canImport(UIKit) + private func seek(by offset: Double) { + let currentSeconds = time.seconds.isFinite ? time.seconds : 0 + let target: Double + if let duration { + target = min(max(currentSeconds + offset, 0), duration) + } else { + target = max(currentSeconds + offset, 0) + } + time = CMTime(seconds: target, preferredTimescale: 600) + } + + private func showSeekFeedback(_ feedback: SeekFeedback) { + seekFeedbackTask?.cancel() + seekFeedback = feedback + seekFeedbackOpacity = 0 + withAnimation(.easeOut(duration: 0.12)) { + seekFeedbackOpacity = 1 + } + seekFeedbackTask = Task { @MainActor in + try? await Task.sleep(nanoseconds: 260_000_000) + guard !Task.isCancelled else { return } + withAnimation(.easeIn(duration: 0.22)) { + seekFeedbackOpacity = 0 + } + try? await Task.sleep(nanoseconds: 240_000_000) + guard !Task.isCancelled else { return } + seekFeedback = nil + } + } + + private var duration: Double? { + switch videoState { + case .playing(let duration), .paused(let duration): + return duration > 0 ? duration : nil + default: + return nil + } + } + + private func beginFastPlayback() { + guard !isFastPlaybackActive else { return } + fastPlaybackWasPlaying = play + playbackResumeTask?.cancel() + play = false + playbackRate = fastPlaybackRate + isFastPlaybackActive = true + playbackResumeTask = Task { @MainActor in + try? await Task.sleep(nanoseconds: 16_000_000) + guard !Task.isCancelled else { return } + play = true + } + } + + private func endFastPlayback() { + playbackResumeTask?.cancel() + playbackResumeTask = nil + guard isFastPlaybackActive else { return } + let shouldResume = fastPlaybackWasPlaying + play = false + playbackRate = normalPlaybackRate + if shouldResume { + playbackResumeTask = Task { @MainActor in + try? await Task.sleep(nanoseconds: 16_000_000) + guard !Task.isCancelled else { return } + play = true + } + } + isFastPlaybackActive = false + } + #endif +} + +#if canImport(UIKit) +private enum SeekFeedback { + case backward + case forward + + var iconName: String { + switch self { + case .backward: + return "gobackward.5" + case .forward: + return "goforward.5" + } + } +} + +private struct VideoGestureOverlay: UIViewRepresentable { + let onDoubleTap: (CGFloat, CGFloat) -> Void + let onLongPressChanged: (Bool) -> Void + + func makeUIView(context: Context) -> UIView { + let view = WindowGestureHostView() + view.backgroundColor = .clear + view.onWindowChanged = { [weak coordinator = context.coordinator, weak view] in + coordinator?.installGestures(from: view) + } + return view + } + + func updateUIView(_ uiView: UIView, context: Context) { + context.coordinator.onDoubleTap = onDoubleTap + context.coordinator.onLongPressChanged = onLongPressChanged + DispatchQueue.main.async { + context.coordinator.installGestures(from: uiView) + } + } + + static func dismantleUIView(_ uiView: UIView, coordinator: Coordinator) { + coordinator.uninstallGestures() + } + + func makeCoordinator() -> Coordinator { + Coordinator(onDoubleTap: onDoubleTap, onLongPressChanged: onLongPressChanged) + } + + final class Coordinator: NSObject, UIGestureRecognizerDelegate { + var onDoubleTap: (CGFloat, CGFloat) -> Void + var onLongPressChanged: (Bool) -> Void + private weak var sourceView: UIView? + private weak var installedWindow: UIWindow? + private var doubleTapRecognizer: UITapGestureRecognizer? + private var longPressRecognizer: UILongPressGestureRecognizer? + private var longPressBeganInside = false + + init( + onDoubleTap: @escaping (CGFloat, CGFloat) -> Void, + onLongPressChanged: @escaping (Bool) -> Void + ) { + self.onDoubleTap = onDoubleTap + self.onLongPressChanged = onLongPressChanged + } + + func installGestures(from view: UIView?) { + sourceView = view + guard let window = view?.window, installedWindow !== window else { return } + uninstallGestures() + sourceView = view + + let doubleTap = UITapGestureRecognizer(target: self, action: #selector(handleDoubleTap(_:))) + doubleTap.numberOfTapsRequired = 2 + doubleTap.cancelsTouchesInView = false + doubleTap.delegate = self + + let longPress = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress(_:))) + longPress.minimumPressDuration = 0.35 + longPress.allowableMovement = 80 + longPress.cancelsTouchesInView = false + longPress.delegate = self + + window.addGestureRecognizer(doubleTap) + window.addGestureRecognizer(longPress) + installedWindow = window + doubleTapRecognizer = doubleTap + longPressRecognizer = longPress + } + + func uninstallGestures() { + if let doubleTapRecognizer { + installedWindow?.removeGestureRecognizer(doubleTapRecognizer) + } + if let longPressRecognizer { + installedWindow?.removeGestureRecognizer(longPressRecognizer) + } + installedWindow = nil + doubleTapRecognizer = nil + longPressRecognizer = nil + longPressBeganInside = false + } + + @objc func handleDoubleTap(_ recognizer: UITapGestureRecognizer) { + guard recognizer.state == .ended, + let window = recognizer.view, + let sourceView, + let location = localLocation(from: recognizer, in: window, sourceView: sourceView) else { return } + onDoubleTap(location.x, sourceView.bounds.width) + } + + @objc func handleLongPress(_ recognizer: UILongPressGestureRecognizer) { + guard let window = recognizer.view, + let sourceView else { return } + + switch recognizer.state { + case .began: + longPressBeganInside = localLocation(from: recognizer, in: window, sourceView: sourceView) != nil + if longPressBeganInside { + onLongPressChanged(true) + } + case .ended, .cancelled, .failed: + if longPressBeganInside { + onLongPressChanged(false) + } + longPressBeganInside = false + default: + break + } + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { + guard let sourceView, + let window = installedWindow else { return false } + let point = touch.location(in: window) + return sourceView.convert(sourceView.bounds, to: window).contains(point) + } + + func gestureRecognizer( + _ gestureRecognizer: UIGestureRecognizer, + shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer + ) -> Bool { + true + } + + private func localLocation( + from recognizer: UIGestureRecognizer, + in windowView: UIView, + sourceView: UIView + ) -> CGPoint? { + let pointInWindow = recognizer.location(in: windowView) + let sourceFrame = sourceView.convert(sourceView.bounds, to: windowView) + guard sourceFrame.contains(pointInWindow) else { return nil } + return sourceView.convert(pointInWindow, from: windowView) + } + } +} + +private final class WindowGestureHostView: UIView { + var onWindowChanged: (() -> Void)? + + override func didMoveToWindow() { + super.didMoveToWindow() + onWindowChanged?() + } + + override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + false + } +} +#endif diff --git a/iosApp/flare/UI/Component/Status/StatusMediaView.swift b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelineItem/StatusMediaView.swift similarity index 91% rename from iosApp/flare/UI/Component/Status/StatusMediaView.swift rename to appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelineItem/StatusMediaView.swift index c6e517adee..cdec9ef6cb 100644 --- a/iosApp/flare/UI/Component/Status/StatusMediaView.swift +++ b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelineItem/StatusMediaView.swift @@ -1,12 +1,11 @@ import SwiftUI import KotlinSharedUI -import TipKit -import LazyPager import SwiftUIBackports +import AppleFontAwesome -struct StatusMediaView: View { - private static let maxVisibleMediaCount = 9 +private let statusMediaMaxVisibleMediaCount = 9 +struct StatusMediaView: View { let data: [any UiMedia] let sensitive: Bool let onMediaClicked: (any UiMedia, Int) -> Void @@ -15,8 +14,21 @@ struct StatusMediaView: View { @State private var isBlur: Bool // @State private var selectedIndex: Int? = nil + init( + data: [any UiMedia], + sensitive: Bool, + cornerRadius: CGFloat, + onMediaClicked: @escaping (any UiMedia, Int) -> Void + ) { + self.data = data + self.sensitive = sensitive + self.onMediaClicked = onMediaClicked + self.cornerRadius = cornerRadius + self._isBlur = State(initialValue: sensitive) + } + var body: some View { - let visibleData = Array(data.prefix(Self.maxVisibleMediaCount)) + let visibleData = Array(data.prefix(statusMediaMaxVisibleMediaCount)) let overflowCount = data.count - visibleData.count AdaptiveGrid( singleFollowsImageAspect: expandMediaSize, @@ -67,7 +79,7 @@ struct StatusMediaView: View { Label { Text("sensitive_button_show", comment: "Button to show sensitive media") } icon: { - Image("fa-eye") + Image(fontAwesome: .eye) .foregroundStyle(.white) } } @@ -80,7 +92,7 @@ struct StatusMediaView: View { isBlur = true } } label: { - Image("fa-eye-slash") + Image(fontAwesome: .eyeSlash) } .backport .glassButtonStyle(fallbackStyle: .bordered) @@ -94,16 +106,6 @@ struct StatusMediaView: View { } } -extension StatusMediaView { - init(data: [any UiMedia], sensitive: Bool, cornerRadius: CGFloat, onMediaClicked: @escaping (any UiMedia, Int) -> Void) { - self.data = data - self.sensitive = sensitive - self.onMediaClicked = onMediaClicked - self.cornerRadius = cornerRadius - self._isBlur = State(initialValue: sensitive) - } -} - private struct MediaOverflowOverlay: View { let count: Int @@ -127,6 +129,7 @@ private extension Int { struct AltTextOverlay: View { let altText: String @State private var showAltText: Bool = false + var body: some View { Button { showAltText = true @@ -145,7 +148,7 @@ struct AltTextOverlay: View { } } -extension UiMedia { +public extension UiMedia { var aspectRatio: CGFloat? { switch onEnum(of: self) { case .image(let image): return CGFloat(image.aspectRatio) diff --git a/iosApp/flare/UI/Component/Status/StatusPollView.swift b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelineItem/StatusPollView.swift similarity index 93% rename from iosApp/flare/UI/Component/Status/StatusPollView.swift rename to appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelineItem/StatusPollView.swift index 8705247051..2e8f6488d8 100644 --- a/iosApp/flare/UI/Component/Status/StatusPollView.swift +++ b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelineItem/StatusPollView.swift @@ -1,12 +1,18 @@ import SwiftUI import KotlinSharedUI import SwiftUIBackports +import FlareAppleCore -struct StatusPollView: View { +public struct StatusPollView: View { @Environment(\.openURL) private var openURL - let data: UiPoll + private let data: UiPoll @State private var selectedOption: [Int] = [] - var body: some View { + + public init(data: UiPoll) { + self.data = data + } + + public var body: some View { VStack( alignment: .trailing ) { @@ -40,7 +46,7 @@ struct StatusPollView: View { .frame(maxWidth: .infinity) .background( RoundedRectangle(cornerRadius: 8, style: .continuous) - .fill(selectedOption.contains(index) ? Color.accentColor.opacity(0.2) : Color(.systemGroupedBackground)) + .fill(selectedOption.contains(index) ? Color.accentColor.opacity(0.2) : Color.flareSystemGroupedBackground) ) } .buttonStyle(.plain) diff --git a/iosApp/flare/UI/Component/Status/StatusReactionView.swift b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelineItem/StatusReactionView.swift similarity index 98% rename from iosApp/flare/UI/Component/Status/StatusReactionView.swift rename to appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelineItem/StatusReactionView.swift index ed510eb566..3bec75a286 100644 --- a/iosApp/flare/UI/Component/Status/StatusReactionView.swift +++ b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelineItem/StatusReactionView.swift @@ -1,7 +1,6 @@ import SwiftUI import KotlinSharedUI -import Flow -import Kingfisher +import FlareAppleCore struct StatusReactionView: View { @Environment(\.openURL) private var openURL @@ -36,7 +35,7 @@ struct StatusReactionView: View { .fixedSize(horizontal: true, vertical: false) } Text(item.count.humanized) - .foregroundStyle(item.me ? Color.white : Color(.label)) + .foregroundStyle(item.me ? Color.white : Color.flareLabel) } } .if(item.me, if: { button in diff --git a/iosApp/flare/UI/Component/Status/StatusTopMessageView.swift b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelineItem/StatusTopMessageView.swift similarity index 98% rename from iosApp/flare/UI/Component/Status/StatusTopMessageView.swift rename to appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelineItem/StatusTopMessageView.swift index bc5980ecf7..204e3c3a41 100644 --- a/iosApp/flare/UI/Component/Status/StatusTopMessageView.swift +++ b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelineItem/StatusTopMessageView.swift @@ -1,10 +1,16 @@ import SwiftUI import KotlinSharedUI +import FlareAppleCore -struct StatusTopMessageView: View { +public struct StatusTopMessageView: View { @Environment(\.openURL) private var openURL - let topMessage: UiTimelineV2.Message - var body: some View { + private let topMessage: UiTimelineV2.Message + + public init(topMessage: UiTimelineV2.Message) { + self.topMessage = topMessage + } + + public var body: some View { HStack { topMessage.icon.image if let user = topMessage.user { @@ -23,7 +29,7 @@ struct StatusTopMessageView: View { } } -extension UiTimelineV2.MessageType { +public extension UiTimelineV2.MessageType { var localizedText: String? { if let data = self as? UiTimelineV2.MessageTypeRaw { return data.content diff --git a/iosApp/flare/UI/Component/Status/StatusView.swift b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelineItem/StatusView.swift similarity index 64% rename from iosApp/flare/UI/Component/Status/StatusView.swift rename to appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelineItem/StatusView.swift index 518d9e2cfe..383b6d3f46 100644 --- a/iosApp/flare/UI/Component/Status/StatusView.swift +++ b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelineItem/StatusView.swift @@ -1,8 +1,10 @@ import SwiftUI import KotlinSharedUI import SwiftUIBackports +import FlareAppleCore +import AppleFontAwesome -struct StatusView: View { +public struct StatusView: View { @Environment(\.timelineAppearance.fullWidthPost) private var fullWidthPost @Environment(\.timelineAppearance.showLinkPreview) private var showLinkPreview @Environment(\.timelineAppearance.compatLinkPreview) private var compatLinkPreview @@ -11,27 +13,78 @@ struct StatusView: View { @Environment(\.timelineAppearance.expandContentWarning) private var expandContentWarning @Environment(\.timelineAppearance.aiConfig.agent) private var agentEnabled @Environment(\.openURL) private var openURL - let data: UiTimelineV2.Post - var isDetail: Bool = false - var isQuote: Bool = false - var withLeadingPadding: Bool = false - var showMedia: Bool = true - var maxLine: Int = 5 - var showExpandTextButton: Bool = true - var forceHideActions: Bool = false - var showTranslate: Bool = true - var showParents: Bool = true + private let data: UiTimelineV2.Post + private let isDetail: Bool + private let isQuote: Bool + private let withLeadingPadding: Bool + private let showMedia: Bool + private let maxLine: Int + private let showExpandTextButton: Bool + private let forceHideActions: Bool + private let showTranslate: Bool + private let showParents: Bool @State private var expand = false + + public init( + data: UiTimelineV2.Post, + isDetail: Bool = false, + isQuote: Bool = false, + withLeadingPadding: Bool = false, + showMedia: Bool = true, + maxLine: Int = 5, + showExpandTextButton: Bool = true, + forceHideActions: Bool = false, + showTranslate: Bool = true, + showParents: Bool = true + ) { + self.data = data + self.isDetail = isDetail + self.isQuote = isQuote + self.withLeadingPadding = withLeadingPadding + self.showMedia = showMedia + self.maxLine = maxLine + self.showExpandTextButton = showExpandTextButton + self.forceHideActions = forceHideActions + self.showTranslate = showTranslate + self.showParents = showParents + } + private var showAsFullWidth: Bool { (!fullWidthPost || withLeadingPadding) && !isQuote && !isDetail } - var body: some View { + public var body: some View { + let parents = Array(data.parents) + let user = data.user + let replyToHandle = data.replyToHandle + let contentWarning = data.contentWarning + let contentWarningIsEmpty = contentWarning?.isEmpty ?? true + let content = data.content + let contentIsEmpty = content.isEmpty + let shouldExpandTextByDefault = data.shouldExpandTextByDefault + let poll = data.poll + let images = Array(data.images) + let hasImages = !images.isEmpty + let sensitive = data.sensitive + let card = data.card + let quotes = Array(data.quote) + let hasQuotes = !quotes.isEmpty + let sourceChannelName = data.sourceChannel?.name + let emojiReactions = Array(data.emojiReactions) + let hasEmojiReactions = !emojiReactions.isEmpty + let visibility = data.visibility + let translationDisplayState = data.translationDisplayState + let platformType = data.platformType + let createdAt = data.createdAt + let actions = Array(data.actions) + let accountType = data.accountType + let statusKey = data.statusKey + VStack( alignment: .leading, spacing: 0 ) { - if !data.parents.isEmpty, showParents { - ForEach(data.parents, id: \.itemKey) { parent in + if !parents.isEmpty, showParents { + ForEach(parents, id: \.itemKey) { parent in VStack( spacing: nil ) { @@ -41,7 +94,7 @@ struct StatusView: View { } .overlay(alignment: .leading) { Rectangle() - .fill(.separator) + .fill(Color.flareSeparator) .frame(minWidth: 1, maxWidth: 1, alignment: .leading) .padding(.leading, 22) .padding(.top, 44) @@ -52,7 +105,7 @@ struct StatusView: View { alignment: .top, spacing: 8, ) { - if showAsFullWidth, let user = data.user { + if showAsFullWidth, let user { AvatarView(data: user.avatar?.url, customHeader: user.avatar?.customHeaders) .frame(width: 44, height: 44) .onTapGesture { @@ -63,22 +116,43 @@ struct StatusView: View { alignment: .leading, spacing: nil, ) { - if let user = data.user { + if let user { if showAsFullWidth { UserOnelineView(data: user, showAvatar: false) { - topEndContent + topEndContent( + visibility: visibility, + translationDisplayState: translationDisplayState, + platformType: platformType, + createdAt: createdAt, + accountType: accountType, + statusKey: statusKey + ) } onClicked: { user.onClicked(ClickContext(launcher: AppleUriLauncher(openUrl: openURL))) } } else if isQuote { UserOnelineView(data: user, showAvatar: true) { - topEndContent + topEndContent( + visibility: visibility, + translationDisplayState: translationDisplayState, + platformType: platformType, + createdAt: createdAt, + accountType: accountType, + statusKey: statusKey + ) } onClicked: { user.onClicked(ClickContext(launcher: AppleUriLauncher(openUrl: openURL))) } } else { UserCompatView(data: user) { - topEndContent + topEndContent( + visibility: visibility, + translationDisplayState: translationDisplayState, + platformType: platformType, + createdAt: createdAt, + accountType: accountType, + statusKey: statusKey + ) } onClicked: { user.onClicked(ClickContext(launcher: AppleUriLauncher(openUrl: openURL))) } @@ -88,15 +162,15 @@ struct StatusView: View { alignment: .leading, spacing: 8, ) { - if let replyToHandle = data.replyToHandle { + if let replyToHandle { HStack { - Image("fa-reply") + Image(fontAwesome: .reply) Text("Reply to \(replyToHandle)") } .font(.caption) .foregroundStyle(.secondary) } - if let contentWarning = data.contentWarning, !contentWarning.isEmpty { + if let contentWarning, !contentWarningIsEmpty { RichText(text: contentWarning) .fixedSize(horizontal: false, vertical: true) .if(isDetail) { view in @@ -120,23 +194,23 @@ struct StatusView: View { } } - if expand || expandContentWarning || data.contentWarning == nil || data.contentWarning?.isEmpty == true { - if !data.content.isEmpty { - RichText(text: data.content) + if expand || expandContentWarning || contentWarningIsEmpty { + if !contentIsEmpty { + RichText(text: content) .frame(maxWidth: .infinity, alignment: .leading) .if(isDetail) { richText in richText .textSelection(.enabled) } else: { richText in richText - .if((data.shouldExpandTextByDefault || expand) && maxLine >= 5, if: { view in + .if((shouldExpandTextByDefault || expand) && maxLine >= 5, if: { view in view.lineLimit(nil) }, else: { view in view.lineLimit(maxLine) }) } .fixedSize(horizontal: false, vertical: true) - if !data.shouldExpandTextByDefault, !isDetail, !expand, showExpandTextButton { + if !shouldExpandTextByDefault, !isDetail, !expand, showExpandTextButton { Button { withAnimation { expand = true @@ -150,15 +224,15 @@ struct StatusView: View { } if isDetail, showTranslate { - StatusTranslateView(content: data.content, contentWarning: data.contentWarning) + StatusTranslateView(content: content, contentWarning: contentWarning) } - if let poll = data.poll, showMedia { + if let poll, showMedia { StatusPollView(data: poll) } - if !data.images.isEmpty, showMedia { - StatusMediaContent(data: data.images, sensitive: data.sensitive, cornerRadius: isQuote ? 12 : 16) { media, index in + if hasImages, showMedia { + StatusMediaContent(data: images, sensitive: sensitive, cornerRadius: isQuote ? 12 : 16) { media, index in let preview: String? = switch onEnum(of: media) { case .image(let image): image.previewUrl @@ -170,8 +244,8 @@ struct StatusView: View { nil } let route = DeeplinkRoute.MediaStatusMedia( - statusKey: data.statusKey, - accountType: data.accountType, + statusKey: statusKey, + accountType: accountType, index: Int32(index), preview: preview ) @@ -181,7 +255,7 @@ struct StatusView: View { } } - if let card = data.card, showMedia, data.images.isEmpty, data.quote.isEmpty, showLinkPreview { + if let card, showMedia, !hasImages, !hasQuotes, showLinkPreview { if compatLinkPreview { StatusCompatCardView(data: card, cornerRadius: isQuote ? 12 : 16) } else { @@ -189,12 +263,11 @@ struct StatusView: View { } } - if !data.quote.isEmpty, !isQuote { + if hasQuotes, !isQuote { VStack { - ForEach(0.. some View { HStack { - if let visibility = data.visibility { + if let visibility { StatusVisibilityView(data: visibility) .font(.caption) .foregroundStyle(.secondary) } - if data.translationDisplayState != .hidden { - TranslateStatusComponent(data: data.translationDisplayState) + if translationDisplayState != .hidden { + TranslateStatusComponent(data: translationDisplayState) .font(.caption) .foregroundStyle(.secondary) } if showPlatformLogo { - switch data.platformType { + switch platformType { case .mastodon: - Image("fa-mastodon") + Image(fontAwesome: .mastodon) .font(.caption) .foregroundStyle(.secondary) case .misskey: - Image("fa-misskey") + Image(fontAwesome: .misskey) .font(.caption) .foregroundStyle(.secondary) case .bluesky: - Image("fa-bluesky") + Image(fontAwesome: .bluesky) .font(.caption) .foregroundStyle(.secondary) case .xQt: - Image("fa-x-twitter") + Image(fontAwesome: .xTwitter) .font(.caption) .foregroundStyle(.secondary) case .vvo: - Image("fa-weibo") + Image(fontAwesome: .weibo) .font(.caption) .foregroundStyle(.secondary) case .nostr: - Image("fa-nostr") + Image(fontAwesome: .nostr) .font(.caption) .foregroundStyle(.secondary) case .pixiv: - Image("fa-pixiv") + Image(fontAwesome: .pixiv) .font(.caption) .foregroundStyle(.secondary) } } if !isDetail { - DateTimeText(data: data.createdAt) + DateTimeText(data: createdAt) .font(.caption) .foregroundStyle(.secondary) } if agentEnabled, !isQuote { Button { let route = DeeplinkRoute.StatusInsight( - accountType: data.accountType, - statusKey: data.statusKey + accountType: accountType, + statusKey: statusKey ) if let url = URL(string: route.toUri()) { openURL(url) } } label: { - Image("fa-robot") + Image(fontAwesome: .robot) .font(.caption) .foregroundStyle(.secondary) } @@ -338,7 +418,7 @@ struct StatusMediaContent: View { Label { Text("show_media_button", comment: "Button to show media attachments" ) } icon: { - Image("fa-image") + Image(fontAwesome: .image) } } .backport diff --git a/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelineItem/StatusVisibilityView.swift b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelineItem/StatusVisibilityView.swift new file mode 100644 index 0000000000..d82ad115b9 --- /dev/null +++ b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelineItem/StatusVisibilityView.swift @@ -0,0 +1,21 @@ +import SwiftUI +import KotlinSharedUI +import AppleFontAwesome + +public struct StatusVisibilityView: View { + private let data: UiTimelineV2.PostVisibility + + public init(data: UiTimelineV2.PostVisibility) { + self.data = data + } + + public var body: some View { + switch data { + case .public: Image(fontAwesome: .globe) + case .home: Image(fontAwesome: .lockOpen) + case .followers: Image(fontAwesome: .lock) + case .specified: Image(fontAwesome: .at) + case .channel: Image(fontAwesome: .tv) + } + } +} diff --git a/iosApp/flare/UI/Component/Status/TimelineUserView.swift b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelineItem/TimelineUserView.swift similarity index 73% rename from iosApp/flare/UI/Component/Status/TimelineUserView.swift rename to appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelineItem/TimelineUserView.swift index 54aa815e66..3d2a33c8fc 100644 --- a/iosApp/flare/UI/Component/Status/TimelineUserView.swift +++ b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelineItem/TimelineUserView.swift @@ -1,12 +1,18 @@ import SwiftUI import KotlinSharedUI +import FlareAppleCore -struct TimelineUserView: View { +public struct TimelineUserView: View { @Environment(\.openURL) private var openURL @Environment(\.timelineAppearance.showNumbers) private var showNumbers - @ScaledMetric(relativeTo: .footnote) var fontSize = 13 - let data: UiTimelineV2.User - var body: some View { + @ScaledMetric(relativeTo: .footnote) private var fontSize = 13 + private let data: UiTimelineV2.User + + public init(data: UiTimelineV2.User) { + self.data = data + } + + public var body: some View { VStack { UserCompatView(data: data.value) .onTapGesture { diff --git a/iosApp/flare/UI/Component/Status/TimelineView.swift b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelineItem/TimelineView.swift similarity index 76% rename from iosApp/flare/UI/Component/Status/TimelineView.swift rename to appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelineItem/TimelineView.swift index f93fa3fff0..9452ca9c19 100644 --- a/iosApp/flare/UI/Component/Status/TimelineView.swift +++ b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelineItem/TimelineView.swift @@ -1,14 +1,21 @@ import SwiftUI import KotlinSharedUI +import FlareAppleCore -struct TimelineView: View { - let data: UiTimelineV2 - let detailStatusKey: MicroBlogKey? - var showTranslate: Bool = true +public struct TimelineView: View { + private let data: UiTimelineV2 + private let detailStatusKey: MicroBlogKey? + private let showTranslate: Bool @Environment(\.timelineAppearance.fullWidthPost) private var fullWidthPost - @ScaledMetric(relativeTo: .caption) var iconSize: CGFloat = 15 + @ScaledMetric(relativeTo: .caption) private var iconSize: CGFloat = 15 - var body: some View { + public init(data: UiTimelineV2, detailStatusKey: MicroBlogKey?, showTranslate: Bool = true) { + self.data = data + self.detailStatusKey = detailStatusKey + self.showTranslate = showTranslate + } + + public var body: some View { switch onEnum(of: data) { case .feed(let feed): FeedView(data: feed) @@ -48,15 +55,16 @@ struct TimelineView: View { } } -extension TimelineView { +public extension TimelineView { init(data: UiTimelineV2) { - self.data = data - self.detailStatusKey = nil + self.init(data: data, detailStatusKey: nil) } } -struct TimelinePlaceholderView: View { - var body: some View { +public struct TimelinePlaceholderView: View { + public init() {} + + public var body: some View { VStack( alignment: .leading, ) { diff --git a/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelineItem/TranslateStatusComponent.swift b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelineItem/TranslateStatusComponent.swift new file mode 100644 index 0000000000..90a4699333 --- /dev/null +++ b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelineItem/TranslateStatusComponent.swift @@ -0,0 +1,22 @@ +import KotlinSharedUI +import SwiftUI +import AppleFontAwesome + +public struct TranslateStatusComponent: View { + private let data: TranslationDisplayState + + public init(data: TranslationDisplayState) { + self.data = data + } + + public var body: some View { + HStack { + Image(fontAwesome: .language) + switch data { + case .failed: Image(fontAwesome: .circleExclamation) + case .translating: ProgressView().progressViewStyle(.circular).scaledToFit().frame(width: 12, height: 12) + default: EmptyView() + } + } + } +} diff --git a/iosApp/flare/UI/Component/Status/TranslateView.swift b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelineItem/TranslateView.swift similarity index 97% rename from iosApp/flare/UI/Component/Status/TranslateView.swift rename to appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelineItem/TranslateView.swift index 6b31eaca8f..256f5a192c 100644 --- a/iosApp/flare/UI/Component/Status/TranslateView.swift +++ b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelineItem/TranslateView.swift @@ -1,5 +1,6 @@ import SwiftUI import KotlinSharedUI +import FlareAppleCore struct StatusTranslateView: View { @Environment(\.aiConfig) private var aiConfig @@ -103,16 +104,16 @@ struct TranslateTextView: View { } -struct TLDRTextView: View { +public struct TLDRTextView: View { @StateObject private var presenter: KotlinPresenter> - init( + public init( text: String ) { self._presenter = .init(wrappedValue: .init(presenter: AiTLDRPresenter(source: text, targetLanguage: Locale.current.language.languageCode?.identifier ?? "en"))) } - var body: some View { + public var body: some View { StateView(state: presenter.state) { text in Text(String(text)) .lineLimit(nil) diff --git a/iosApp/flare/UI/Component/UserCompatView.swift b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelineItem/UserCompatView.swift similarity index 77% rename from iosApp/flare/UI/Component/UserCompatView.swift rename to appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelineItem/UserCompatView.swift index 5712e8510d..0d382b0623 100644 --- a/iosApp/flare/UI/Component/UserCompatView.swift +++ b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelineItem/UserCompatView.swift @@ -1,12 +1,23 @@ import SwiftUI import KotlinSharedUI +import FlareAppleCore -struct UserCompatView: View { - @Environment(\.openURL) private var openURL - let data: UiProfile - let trailing: () -> TrailingContent - let onClicked: (() -> Void)? - var body: some View { +public struct UserCompatView: View { + private let data: UiProfile + private let trailing: () -> TrailingContent + private let onClicked: (() -> Void)? + + public init( + data: UiProfile, + @ViewBuilder trailing: @escaping () -> TrailingContent, + onClicked: (() -> Void)? = nil + ) { + self.data = data + self.trailing = trailing + self.onClicked = onClicked + } + + public var body: some View { HStack { AvatarView(data: data.avatar?.url, customHeader: data.avatar?.customHeaders) .frame(width: 44, height: 44) @@ -37,18 +48,18 @@ struct UserCompatView: View { } } -extension UserCompatView { +public extension UserCompatView { init(data: UiProfile) where TrailingContent == EmptyView { - self.data = data - self.trailing = { + self.init(data: data) { EmptyView() } - self.onClicked = nil } } -struct UserLoadingView: View { - var body: some View { +public struct UserLoadingView: View { + public init() {} + + public var body: some View { HStack { Rectangle() .fill(.placeholder) @@ -68,9 +79,14 @@ struct UserLoadingView: View { } } -struct UserErrorView: View { - let error: KotlinThrowable - var body: some View { +public struct UserErrorView: View { + private let error: KotlinThrowable + + public init(error: KotlinThrowable) { + self.error = error + } + + public var body: some View { if let expiredError = error as? LoginExpiredException { HStack { Image(systemName: "person.badge.shield.exclamationmark") diff --git a/iosApp/flare/UI/Component/Status/UserListView.swift b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelineItem/UserListView.swift similarity index 89% rename from iosApp/flare/UI/Component/Status/UserListView.swift rename to appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelineItem/UserListView.swift index 635bdb22f6..523514da23 100644 --- a/iosApp/flare/UI/Component/Status/UserListView.swift +++ b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelineItem/UserListView.swift @@ -1,9 +1,11 @@ import SwiftUI import KotlinSharedUI +import FlareAppleCore struct UserListView: View { @Environment(\.openURL) private var openURL let data: UiTimelineV2.UserList + var body: some View { VStack { ScrollView(.horizontal) { @@ -15,7 +17,7 @@ struct UserListView: View { .clipShape(.rect(cornerRadius: 16)) .overlay( RoundedRectangle(cornerRadius: 16) - .stroke(Color(.separator), lineWidth: 1) + .stroke(Color.flareSeparator, lineWidth: 1) ) .onTapGesture { user.onClicked(ClickContext(launcher: AppleUriLauncher(openUrl: openURL))) @@ -33,7 +35,7 @@ struct UserListView: View { .clipShape(.rect(cornerRadius: 16)) .overlay( RoundedRectangle(cornerRadius: 16) - .stroke(Color(.separator), lineWidth: 1) + .stroke(Color.flareSeparator, lineWidth: 1) ) } } diff --git a/iosApp/flare/UI/Component/UserOnelineView.swift b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelineItem/UserOnelineView.swift similarity index 56% rename from iosApp/flare/UI/Component/UserOnelineView.swift rename to appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelineItem/UserOnelineView.swift index 319e579da9..ab38fd13c6 100644 --- a/iosApp/flare/UI/Component/UserOnelineView.swift +++ b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelineItem/UserOnelineView.swift @@ -1,13 +1,25 @@ import SwiftUI import KotlinSharedUI -struct UserOnelineView: View { - @Environment(\.openURL) private var openURL - let data: UiProfile - let showAvatar: Bool - let trailing: () -> TrailingContent - let onClicked: (() -> Void)? - var body: some View { +public struct UserOnelineView: View { + private let data: UiProfile + private let showAvatar: Bool + private let trailing: () -> TrailingContent + private let onClicked: (() -> Void)? + + public init( + data: UiProfile, + showAvatar: Bool, + @ViewBuilder trailing: @escaping () -> TrailingContent, + onClicked: (() -> Void)? = nil + ) { + self.data = data + self.showAvatar = showAvatar + self.trailing = trailing + self.onClicked = onClicked + } + + public var body: some View { HStack { if showAvatar { AvatarView(data: data.avatar?.url, customHeader: data.avatar?.customHeaders) @@ -29,13 +41,10 @@ struct UserOnelineView: View { } } -extension UserOnelineView { +public extension UserOnelineView { init(data: UiProfile) where TrailingContent == EmptyView { - self.data = data - self.trailing = { + self.init(data: data, showAvatar: true) { EmptyView() } - self.onClicked = nil - self.showAvatar = true } } diff --git a/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelinePagingView.swift b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelinePagingView.swift new file mode 100644 index 0000000000..a4b644f8bc --- /dev/null +++ b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/TimelinePagingView.swift @@ -0,0 +1,129 @@ +import SwiftUI +import KotlinSharedUI + +public struct TimelinePagingView: View { + private let data: PagingState + private let detailStatusKey: MicroBlogKey? + private let loadingCount = 5 + + public init(data: PagingState, detailStatusKey: MicroBlogKey? = nil) { + self.data = data + self.detailStatusKey = detailStatusKey + } + + public var body: some View { + switch onEnum(of: data) { + case .empty: + ListEmptyView() + case .error(let error): + ListErrorView(error: error.error) { + _ = error.onRetry() + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) + case .loading: + LazyVStack(spacing: 0) { + ForEach(0..) -> some View { + let count = Int(success.itemCount) + let rows = TimelinePagingRows(success: success, count: count) + LazyVStack(spacing: 0) { + ForEach(rows) { row in + TimelinePagingRowView( + row: row, + totalCount: count, + detailStatusKey: detailStatusKey, + onDisplay: { index in + _ = success.get(index: Int32(index)) + } + ) + } + + switch onEnum(of: success.appendState) { + case .error(let error): + ListErrorView(error: error.error) { + success.retry() + } + .frame(maxWidth: .infinity, alignment: .center) + case .loading: + ProgressView() + .padding() + .frame(maxWidth: .infinity, alignment: .center) + case .notLoading: + EmptyView() + } + } + } + + private func loadingRow(index: Int, totalCount: Int) -> some View { + AdaptiveTimelineCard(index: index, totalCount: totalCount) { + TimelinePlaceholderView() + .padding(.horizontal) + .padding(.vertical, 12) + } + } +} + +private struct TimelinePagingRow: Identifiable { + let id: String + let index: Int + let item: UiTimelineV2? +} + +private struct TimelinePagingRowView: View { + let row: TimelinePagingRow + let totalCount: Int + let detailStatusKey: MicroBlogKey? + let onDisplay: (Int) -> Void + + var body: some View { + Group { + if let item = row.item { + AdaptiveTimelineCard(index: row.index, totalCount: totalCount) { + TimelineView(data: item, detailStatusKey: detailStatusKey) + .padding(.horizontal) + .padding(.vertical, 12) + } + } else { + AdaptiveTimelineCard(index: row.index, totalCount: totalCount) { + TimelinePlaceholderView() + .padding(.horizontal) + .padding(.vertical, 12) + } + } + } + .onAppear { + onDisplay(row.index) + } + } +} + +private struct TimelinePagingRows: @MainActor RandomAccessCollection { + let success: PagingStateSuccess + let count: Int + + var startIndex: Int { 0 } + var endIndex: Int { count } + + func index(after index: Int) -> Int { index + 1 } + func index(before index: Int) -> Int { index - 1 } + func index(_ index: Int, offsetBy distance: Int) -> Int { index + distance } + func distance(from start: Int, to end: Int) -> Int { end - start } + + subscript(position: Int) -> TimelinePagingRow { + let item = success.peek(index: Int32(position)) + return TimelinePagingRow( + id: item?.itemKey ?? "placeholder-\(position)", + index: position, + item: item + ) + } +} diff --git a/iosApp/flare/UI/Screen/UserListScreen.swift b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/UserListScreen.swift similarity index 89% rename from iosApp/flare/UI/Screen/UserListScreen.swift rename to appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/UserListScreen.swift index 9e7aafa69a..bc546c2c7f 100644 --- a/iosApp/flare/UI/Screen/UserListScreen.swift +++ b/appleApp/Shared/FlareAppleCore/Sources/FlareAppleUI/UserListScreen.swift @@ -1,11 +1,26 @@ -import SwiftUI +import FlareAppleCore @preconcurrency import KotlinSharedUI +import SwiftUI -struct UserListScreen: View { +public struct UserListScreen: View { @Environment(\.openURL) private var openURL @StateObject private var presenter: KotlinPresenter private let isFollowing: Bool - var body: some View { + + public init( + accountType: AccountType, + userKey: MicroBlogKey, + isFollowing: Bool + ) { + self.isFollowing = isFollowing + if isFollowing { + self._presenter = .init(wrappedValue: .init(presenter: FollowingPresenter(accountType: accountType, userKey: userKey))) + } else { + self._presenter = .init(wrappedValue: .init(presenter: FansPresenter(accountType: accountType, userKey: userKey))) + } + } + + public var body: some View { List { PagingView(data: presenter.state.listState) { item in UserCompatView(data: item) @@ -15,7 +30,6 @@ struct UserListScreen: View { } loadingContent: { UserLoadingView() } - } .refreshable { try? await presenter.state.refreshSuspend() @@ -23,19 +37,3 @@ struct UserListScreen: View { .navigationTitle(isFollowing ? "user_list_title_following" : "user_list_title_fans") } } - - -extension UserListScreen { - init( - accountType: AccountType, - userKey: MicroBlogKey, - isFollowing: Bool, - ) { - self.isFollowing = isFollowing - if isFollowing { - self._presenter = .init(wrappedValue: .init(presenter: FollowingPresenter(accountType: accountType, userKey: userKey))) - } else { - self._presenter = .init(wrappedValue: .init(presenter: FansPresenter(accountType: accountType, userKey: userKey))) - } - } -} diff --git a/iosApp/flare/AppIcon.icon/Assets/Group-2.svg b/appleApp/ios/AppIcon.icon/Assets/Group-2.svg similarity index 100% rename from iosApp/flare/AppIcon.icon/Assets/Group-2.svg rename to appleApp/ios/AppIcon.icon/Assets/Group-2.svg diff --git a/iosApp/flare/AppIcon.icon/Assets/Group.svg b/appleApp/ios/AppIcon.icon/Assets/Group.svg similarity index 100% rename from iosApp/flare/AppIcon.icon/Assets/Group.svg rename to appleApp/ios/AppIcon.icon/Assets/Group.svg diff --git a/iosApp/flare/AppIcon.icon/icon.json b/appleApp/ios/AppIcon.icon/icon.json similarity index 100% rename from iosApp/flare/AppIcon.icon/icon.json rename to appleApp/ios/AppIcon.icon/icon.json diff --git a/iosApp/flare/AppIcon_black.icon/Assets/Group-2.svg b/appleApp/ios/AppIcon_black.icon/Assets/Group-2.svg similarity index 100% rename from iosApp/flare/AppIcon_black.icon/Assets/Group-2.svg rename to appleApp/ios/AppIcon_black.icon/Assets/Group-2.svg diff --git a/iosApp/flare/AppIcon_black.icon/Assets/Group.svg b/appleApp/ios/AppIcon_black.icon/Assets/Group.svg similarity index 100% rename from iosApp/flare/AppIcon_black.icon/Assets/Group.svg rename to appleApp/ios/AppIcon_black.icon/Assets/Group.svg diff --git a/iosApp/flare/AppIcon_black.icon/icon.json b/appleApp/ios/AppIcon_black.icon/icon.json similarity index 100% rename from iosApp/flare/AppIcon_black.icon/icon.json rename to appleApp/ios/AppIcon_black.icon/icon.json diff --git a/iosApp/flare/AppIcon_blue.icon/Assets/Group-2.svg b/appleApp/ios/AppIcon_blue.icon/Assets/Group-2.svg similarity index 100% rename from iosApp/flare/AppIcon_blue.icon/Assets/Group-2.svg rename to appleApp/ios/AppIcon_blue.icon/Assets/Group-2.svg diff --git a/iosApp/flare/AppIcon_blue.icon/Assets/Group.svg b/appleApp/ios/AppIcon_blue.icon/Assets/Group.svg similarity index 100% rename from iosApp/flare/AppIcon_blue.icon/Assets/Group.svg rename to appleApp/ios/AppIcon_blue.icon/Assets/Group.svg diff --git a/iosApp/flare/AppIcon_blue.icon/icon.json b/appleApp/ios/AppIcon_blue.icon/icon.json similarity index 100% rename from iosApp/flare/AppIcon_blue.icon/icon.json rename to appleApp/ios/AppIcon_blue.icon/icon.json diff --git a/iosApp/flare/AppIcon_cyan.icon/Assets/Group-2.svg b/appleApp/ios/AppIcon_cyan.icon/Assets/Group-2.svg similarity index 100% rename from iosApp/flare/AppIcon_cyan.icon/Assets/Group-2.svg rename to appleApp/ios/AppIcon_cyan.icon/Assets/Group-2.svg diff --git a/iosApp/flare/AppIcon_cyan.icon/Assets/Group.svg b/appleApp/ios/AppIcon_cyan.icon/Assets/Group.svg similarity index 100% rename from iosApp/flare/AppIcon_cyan.icon/Assets/Group.svg rename to appleApp/ios/AppIcon_cyan.icon/Assets/Group.svg diff --git a/iosApp/flare/AppIcon_cyan.icon/icon.json b/appleApp/ios/AppIcon_cyan.icon/icon.json similarity index 100% rename from iosApp/flare/AppIcon_cyan.icon/icon.json rename to appleApp/ios/AppIcon_cyan.icon/icon.json diff --git a/iosApp/flare/AppIcon_light_blue.icon/Assets/Group-2.svg b/appleApp/ios/AppIcon_light_blue.icon/Assets/Group-2.svg similarity index 100% rename from iosApp/flare/AppIcon_light_blue.icon/Assets/Group-2.svg rename to appleApp/ios/AppIcon_light_blue.icon/Assets/Group-2.svg diff --git a/iosApp/flare/AppIcon_light_blue.icon/Assets/Group.svg b/appleApp/ios/AppIcon_light_blue.icon/Assets/Group.svg similarity index 100% rename from iosApp/flare/AppIcon_light_blue.icon/Assets/Group.svg rename to appleApp/ios/AppIcon_light_blue.icon/Assets/Group.svg diff --git a/iosApp/flare/AppIcon_light_blue.icon/icon.json b/appleApp/ios/AppIcon_light_blue.icon/icon.json similarity index 100% rename from iosApp/flare/AppIcon_light_blue.icon/icon.json rename to appleApp/ios/AppIcon_light_blue.icon/icon.json diff --git a/iosApp/flare/AppIcon_orange.icon/Assets/Group-2.svg b/appleApp/ios/AppIcon_orange.icon/Assets/Group-2.svg similarity index 100% rename from iosApp/flare/AppIcon_orange.icon/Assets/Group-2.svg rename to appleApp/ios/AppIcon_orange.icon/Assets/Group-2.svg diff --git a/iosApp/flare/AppIcon_orange.icon/Assets/Group.svg b/appleApp/ios/AppIcon_orange.icon/Assets/Group.svg similarity index 100% rename from iosApp/flare/AppIcon_orange.icon/Assets/Group.svg rename to appleApp/ios/AppIcon_orange.icon/Assets/Group.svg diff --git a/iosApp/flare/AppIcon_orange.icon/icon.json b/appleApp/ios/AppIcon_orange.icon/icon.json similarity index 100% rename from iosApp/flare/AppIcon_orange.icon/icon.json rename to appleApp/ios/AppIcon_orange.icon/icon.json diff --git a/iosApp/flare/AppIcon_red.icon/Assets/Group-2.svg b/appleApp/ios/AppIcon_red.icon/Assets/Group-2.svg similarity index 100% rename from iosApp/flare/AppIcon_red.icon/Assets/Group-2.svg rename to appleApp/ios/AppIcon_red.icon/Assets/Group-2.svg diff --git a/iosApp/flare/AppIcon_red.icon/Assets/Group.svg b/appleApp/ios/AppIcon_red.icon/Assets/Group.svg similarity index 100% rename from iosApp/flare/AppIcon_red.icon/Assets/Group.svg rename to appleApp/ios/AppIcon_red.icon/Assets/Group.svg diff --git a/iosApp/flare/AppIcon_red.icon/icon.json b/appleApp/ios/AppIcon_red.icon/icon.json similarity index 100% rename from iosApp/flare/AppIcon_red.icon/icon.json rename to appleApp/ios/AppIcon_red.icon/icon.json diff --git a/iosApp/flare/AppIcon_teal.icon/Assets/Group-2.svg b/appleApp/ios/AppIcon_teal.icon/Assets/Group-2.svg similarity index 100% rename from iosApp/flare/AppIcon_teal.icon/Assets/Group-2.svg rename to appleApp/ios/AppIcon_teal.icon/Assets/Group-2.svg diff --git a/iosApp/flare/AppIcon_teal.icon/Assets/Group.svg b/appleApp/ios/AppIcon_teal.icon/Assets/Group.svg similarity index 100% rename from iosApp/flare/AppIcon_teal.icon/Assets/Group.svg rename to appleApp/ios/AppIcon_teal.icon/Assets/Group.svg diff --git a/iosApp/flare/AppIcon_teal.icon/icon.json b/appleApp/ios/AppIcon_teal.icon/icon.json similarity index 100% rename from iosApp/flare/AppIcon_teal.icon/icon.json rename to appleApp/ios/AppIcon_teal.icon/icon.json diff --git a/iosApp/flare/AppIcon_white.icon/Assets/Group-2.svg b/appleApp/ios/AppIcon_white.icon/Assets/Group-2.svg similarity index 100% rename from iosApp/flare/AppIcon_white.icon/Assets/Group-2.svg rename to appleApp/ios/AppIcon_white.icon/Assets/Group-2.svg diff --git a/iosApp/flare/AppIcon_white.icon/Assets/Group.svg b/appleApp/ios/AppIcon_white.icon/Assets/Group.svg similarity index 100% rename from iosApp/flare/AppIcon_white.icon/Assets/Group.svg rename to appleApp/ios/AppIcon_white.icon/Assets/Group.svg diff --git a/iosApp/flare/AppIcon_white.icon/icon.json b/appleApp/ios/AppIcon_white.icon/icon.json similarity index 100% rename from iosApp/flare/AppIcon_white.icon/icon.json rename to appleApp/ios/AppIcon_white.icon/icon.json diff --git a/iosApp/flare/AppIcon_yellow.icon/Assets/Group-2.svg b/appleApp/ios/AppIcon_yellow.icon/Assets/Group-2.svg similarity index 100% rename from iosApp/flare/AppIcon_yellow.icon/Assets/Group-2.svg rename to appleApp/ios/AppIcon_yellow.icon/Assets/Group-2.svg diff --git a/iosApp/flare/AppIcon_yellow.icon/Assets/Group.svg b/appleApp/ios/AppIcon_yellow.icon/Assets/Group.svg similarity index 100% rename from iosApp/flare/AppIcon_yellow.icon/Assets/Group.svg rename to appleApp/ios/AppIcon_yellow.icon/Assets/Group.svg diff --git a/iosApp/flare/AppIcon_yellow.icon/icon.json b/appleApp/ios/AppIcon_yellow.icon/icon.json similarity index 100% rename from iosApp/flare/AppIcon_yellow.icon/icon.json rename to appleApp/ios/AppIcon_yellow.icon/icon.json diff --git a/iosApp/flare/Assets.xcassets/AccentColor.colorset/Contents.json b/appleApp/ios/Assets.xcassets/AccentColor.colorset/Contents.json similarity index 100% rename from iosApp/flare/Assets.xcassets/AccentColor.colorset/Contents.json rename to appleApp/ios/Assets.xcassets/AccentColor.colorset/Contents.json diff --git a/iosApp/flare/Assets.xcassets/AppIcon_black.appiconset/AppIcon_black.png b/appleApp/ios/Assets.xcassets/AppIcon_black.appiconset/AppIcon_black.png similarity index 100% rename from iosApp/flare/Assets.xcassets/AppIcon_black.appiconset/AppIcon_black.png rename to appleApp/ios/Assets.xcassets/AppIcon_black.appiconset/AppIcon_black.png diff --git a/iosApp/flare/Assets.xcassets/AppIcon_black.appiconset/Contents.json b/appleApp/ios/Assets.xcassets/AppIcon_black.appiconset/Contents.json similarity index 100% rename from iosApp/flare/Assets.xcassets/AppIcon_black.appiconset/Contents.json rename to appleApp/ios/Assets.xcassets/AppIcon_black.appiconset/Contents.json diff --git a/iosApp/flare/Assets.xcassets/AppIcon_blue.appiconset/AppIcon_blue.png b/appleApp/ios/Assets.xcassets/AppIcon_blue.appiconset/AppIcon_blue.png similarity index 100% rename from iosApp/flare/Assets.xcassets/AppIcon_blue.appiconset/AppIcon_blue.png rename to appleApp/ios/Assets.xcassets/AppIcon_blue.appiconset/AppIcon_blue.png diff --git a/iosApp/flare/Assets.xcassets/AppIcon_blue.appiconset/Contents.json b/appleApp/ios/Assets.xcassets/AppIcon_blue.appiconset/Contents.json similarity index 100% rename from iosApp/flare/Assets.xcassets/AppIcon_blue.appiconset/Contents.json rename to appleApp/ios/Assets.xcassets/AppIcon_blue.appiconset/Contents.json diff --git a/iosApp/flare/Assets.xcassets/AppIcon_cyan.appiconset/AppIcon_cyan.png b/appleApp/ios/Assets.xcassets/AppIcon_cyan.appiconset/AppIcon_cyan.png similarity index 100% rename from iosApp/flare/Assets.xcassets/AppIcon_cyan.appiconset/AppIcon_cyan.png rename to appleApp/ios/Assets.xcassets/AppIcon_cyan.appiconset/AppIcon_cyan.png diff --git a/iosApp/flare/Assets.xcassets/AppIcon_cyan.appiconset/Contents.json b/appleApp/ios/Assets.xcassets/AppIcon_cyan.appiconset/Contents.json similarity index 100% rename from iosApp/flare/Assets.xcassets/AppIcon_cyan.appiconset/Contents.json rename to appleApp/ios/Assets.xcassets/AppIcon_cyan.appiconset/Contents.json diff --git a/iosApp/flare/Assets.xcassets/AppIcon_light_blue.appiconset/AppIcon_light_blue.png b/appleApp/ios/Assets.xcassets/AppIcon_light_blue.appiconset/AppIcon_light_blue.png similarity index 100% rename from iosApp/flare/Assets.xcassets/AppIcon_light_blue.appiconset/AppIcon_light_blue.png rename to appleApp/ios/Assets.xcassets/AppIcon_light_blue.appiconset/AppIcon_light_blue.png diff --git a/iosApp/flare/Assets.xcassets/AppIcon_light_blue.appiconset/Contents.json b/appleApp/ios/Assets.xcassets/AppIcon_light_blue.appiconset/Contents.json similarity index 100% rename from iosApp/flare/Assets.xcassets/AppIcon_light_blue.appiconset/Contents.json rename to appleApp/ios/Assets.xcassets/AppIcon_light_blue.appiconset/Contents.json diff --git a/iosApp/flare/Assets.xcassets/AppIcon_orange.appiconset/AppIcon_orange.png b/appleApp/ios/Assets.xcassets/AppIcon_orange.appiconset/AppIcon_orange.png similarity index 100% rename from iosApp/flare/Assets.xcassets/AppIcon_orange.appiconset/AppIcon_orange.png rename to appleApp/ios/Assets.xcassets/AppIcon_orange.appiconset/AppIcon_orange.png diff --git a/iosApp/flare/Assets.xcassets/AppIcon_orange.appiconset/Contents.json b/appleApp/ios/Assets.xcassets/AppIcon_orange.appiconset/Contents.json similarity index 100% rename from iosApp/flare/Assets.xcassets/AppIcon_orange.appiconset/Contents.json rename to appleApp/ios/Assets.xcassets/AppIcon_orange.appiconset/Contents.json diff --git a/iosApp/flare/Assets.xcassets/AppIcon_red.appiconset/AppIcon_red.png b/appleApp/ios/Assets.xcassets/AppIcon_red.appiconset/AppIcon_red.png similarity index 100% rename from iosApp/flare/Assets.xcassets/AppIcon_red.appiconset/AppIcon_red.png rename to appleApp/ios/Assets.xcassets/AppIcon_red.appiconset/AppIcon_red.png diff --git a/iosApp/flare/Assets.xcassets/AppIcon_red.appiconset/Contents.json b/appleApp/ios/Assets.xcassets/AppIcon_red.appiconset/Contents.json similarity index 100% rename from iosApp/flare/Assets.xcassets/AppIcon_red.appiconset/Contents.json rename to appleApp/ios/Assets.xcassets/AppIcon_red.appiconset/Contents.json diff --git a/iosApp/flare/Assets.xcassets/AppIcon_teal.appiconset/AppIcon_teal.png b/appleApp/ios/Assets.xcassets/AppIcon_teal.appiconset/AppIcon_teal.png similarity index 100% rename from iosApp/flare/Assets.xcassets/AppIcon_teal.appiconset/AppIcon_teal.png rename to appleApp/ios/Assets.xcassets/AppIcon_teal.appiconset/AppIcon_teal.png diff --git a/iosApp/flare/Assets.xcassets/AppIcon_teal.appiconset/Contents.json b/appleApp/ios/Assets.xcassets/AppIcon_teal.appiconset/Contents.json similarity index 100% rename from iosApp/flare/Assets.xcassets/AppIcon_teal.appiconset/Contents.json rename to appleApp/ios/Assets.xcassets/AppIcon_teal.appiconset/Contents.json diff --git a/iosApp/flare/Assets.xcassets/AppIcon_white.appiconset/AppIcon_white.png b/appleApp/ios/Assets.xcassets/AppIcon_white.appiconset/AppIcon_white.png similarity index 100% rename from iosApp/flare/Assets.xcassets/AppIcon_white.appiconset/AppIcon_white.png rename to appleApp/ios/Assets.xcassets/AppIcon_white.appiconset/AppIcon_white.png diff --git a/iosApp/flare/Assets.xcassets/AppIcon_white.appiconset/Contents.json b/appleApp/ios/Assets.xcassets/AppIcon_white.appiconset/Contents.json similarity index 100% rename from iosApp/flare/Assets.xcassets/AppIcon_white.appiconset/Contents.json rename to appleApp/ios/Assets.xcassets/AppIcon_white.appiconset/Contents.json diff --git a/iosApp/flare/Assets.xcassets/AppIcon_yellow.appiconset/AppIcon_yellow.png b/appleApp/ios/Assets.xcassets/AppIcon_yellow.appiconset/AppIcon_yellow.png similarity index 100% rename from iosApp/flare/Assets.xcassets/AppIcon_yellow.appiconset/AppIcon_yellow.png rename to appleApp/ios/Assets.xcassets/AppIcon_yellow.appiconset/AppIcon_yellow.png diff --git a/iosApp/flare/Assets.xcassets/AppIcon_yellow.appiconset/Contents.json b/appleApp/ios/Assets.xcassets/AppIcon_yellow.appiconset/Contents.json similarity index 100% rename from iosApp/flare/Assets.xcassets/AppIcon_yellow.appiconset/Contents.json rename to appleApp/ios/Assets.xcassets/AppIcon_yellow.appiconset/Contents.json diff --git a/iosApp/flare/Assets.xcassets/Contents.json b/appleApp/ios/Assets.xcassets/Contents.json similarity index 100% rename from iosApp/flare/Assets.xcassets/Contents.json rename to appleApp/ios/Assets.xcassets/Contents.json diff --git a/iosApp/flare/Assets.xcassets/app_icon_preview_black.imageset/Contents.json b/appleApp/ios/Assets.xcassets/app_icon_preview_black.imageset/Contents.json similarity index 100% rename from iosApp/flare/Assets.xcassets/app_icon_preview_black.imageset/Contents.json rename to appleApp/ios/Assets.xcassets/app_icon_preview_black.imageset/Contents.json diff --git a/iosApp/flare/Assets.xcassets/app_icon_preview_black.imageset/app_icon_preview_black.png b/appleApp/ios/Assets.xcassets/app_icon_preview_black.imageset/app_icon_preview_black.png similarity index 100% rename from iosApp/flare/Assets.xcassets/app_icon_preview_black.imageset/app_icon_preview_black.png rename to appleApp/ios/Assets.xcassets/app_icon_preview_black.imageset/app_icon_preview_black.png diff --git a/iosApp/flare/Assets.xcassets/app_icon_preview_blue.imageset/Contents.json b/appleApp/ios/Assets.xcassets/app_icon_preview_blue.imageset/Contents.json similarity index 100% rename from iosApp/flare/Assets.xcassets/app_icon_preview_blue.imageset/Contents.json rename to appleApp/ios/Assets.xcassets/app_icon_preview_blue.imageset/Contents.json diff --git a/iosApp/flare/Assets.xcassets/app_icon_preview_blue.imageset/app_icon_preview_blue.png b/appleApp/ios/Assets.xcassets/app_icon_preview_blue.imageset/app_icon_preview_blue.png similarity index 100% rename from iosApp/flare/Assets.xcassets/app_icon_preview_blue.imageset/app_icon_preview_blue.png rename to appleApp/ios/Assets.xcassets/app_icon_preview_blue.imageset/app_icon_preview_blue.png diff --git a/iosApp/flare/Assets.xcassets/app_icon_preview_cyan.imageset/Contents.json b/appleApp/ios/Assets.xcassets/app_icon_preview_cyan.imageset/Contents.json similarity index 100% rename from iosApp/flare/Assets.xcassets/app_icon_preview_cyan.imageset/Contents.json rename to appleApp/ios/Assets.xcassets/app_icon_preview_cyan.imageset/Contents.json diff --git a/iosApp/flare/Assets.xcassets/app_icon_preview_cyan.imageset/app_icon_preview_cyan.png b/appleApp/ios/Assets.xcassets/app_icon_preview_cyan.imageset/app_icon_preview_cyan.png similarity index 100% rename from iosApp/flare/Assets.xcassets/app_icon_preview_cyan.imageset/app_icon_preview_cyan.png rename to appleApp/ios/Assets.xcassets/app_icon_preview_cyan.imageset/app_icon_preview_cyan.png diff --git a/iosApp/flare/Assets.xcassets/app_icon_preview_default.imageset/Contents.json b/appleApp/ios/Assets.xcassets/app_icon_preview_default.imageset/Contents.json similarity index 100% rename from iosApp/flare/Assets.xcassets/app_icon_preview_default.imageset/Contents.json rename to appleApp/ios/Assets.xcassets/app_icon_preview_default.imageset/Contents.json diff --git a/iosApp/flare/Assets.xcassets/app_icon_preview_default.imageset/app_icon_preview_default.png b/appleApp/ios/Assets.xcassets/app_icon_preview_default.imageset/app_icon_preview_default.png similarity index 100% rename from iosApp/flare/Assets.xcassets/app_icon_preview_default.imageset/app_icon_preview_default.png rename to appleApp/ios/Assets.xcassets/app_icon_preview_default.imageset/app_icon_preview_default.png diff --git a/iosApp/flare/Assets.xcassets/app_icon_preview_light_blue.imageset/Contents.json b/appleApp/ios/Assets.xcassets/app_icon_preview_light_blue.imageset/Contents.json similarity index 100% rename from iosApp/flare/Assets.xcassets/app_icon_preview_light_blue.imageset/Contents.json rename to appleApp/ios/Assets.xcassets/app_icon_preview_light_blue.imageset/Contents.json diff --git a/iosApp/flare/Assets.xcassets/app_icon_preview_light_blue.imageset/app_icon_preview_light_blue.png b/appleApp/ios/Assets.xcassets/app_icon_preview_light_blue.imageset/app_icon_preview_light_blue.png similarity index 100% rename from iosApp/flare/Assets.xcassets/app_icon_preview_light_blue.imageset/app_icon_preview_light_blue.png rename to appleApp/ios/Assets.xcassets/app_icon_preview_light_blue.imageset/app_icon_preview_light_blue.png diff --git a/iosApp/flare/Assets.xcassets/app_icon_preview_orange.imageset/Contents.json b/appleApp/ios/Assets.xcassets/app_icon_preview_orange.imageset/Contents.json similarity index 100% rename from iosApp/flare/Assets.xcassets/app_icon_preview_orange.imageset/Contents.json rename to appleApp/ios/Assets.xcassets/app_icon_preview_orange.imageset/Contents.json diff --git a/iosApp/flare/Assets.xcassets/app_icon_preview_orange.imageset/app_icon_preview_orange.png b/appleApp/ios/Assets.xcassets/app_icon_preview_orange.imageset/app_icon_preview_orange.png similarity index 100% rename from iosApp/flare/Assets.xcassets/app_icon_preview_orange.imageset/app_icon_preview_orange.png rename to appleApp/ios/Assets.xcassets/app_icon_preview_orange.imageset/app_icon_preview_orange.png diff --git a/iosApp/flare/Assets.xcassets/app_icon_preview_red.imageset/Contents.json b/appleApp/ios/Assets.xcassets/app_icon_preview_red.imageset/Contents.json similarity index 100% rename from iosApp/flare/Assets.xcassets/app_icon_preview_red.imageset/Contents.json rename to appleApp/ios/Assets.xcassets/app_icon_preview_red.imageset/Contents.json diff --git a/iosApp/flare/Assets.xcassets/app_icon_preview_red.imageset/app_icon_preview_red.png b/appleApp/ios/Assets.xcassets/app_icon_preview_red.imageset/app_icon_preview_red.png similarity index 100% rename from iosApp/flare/Assets.xcassets/app_icon_preview_red.imageset/app_icon_preview_red.png rename to appleApp/ios/Assets.xcassets/app_icon_preview_red.imageset/app_icon_preview_red.png diff --git a/iosApp/flare/Assets.xcassets/app_icon_preview_teal.imageset/Contents.json b/appleApp/ios/Assets.xcassets/app_icon_preview_teal.imageset/Contents.json similarity index 100% rename from iosApp/flare/Assets.xcassets/app_icon_preview_teal.imageset/Contents.json rename to appleApp/ios/Assets.xcassets/app_icon_preview_teal.imageset/Contents.json diff --git a/iosApp/flare/Assets.xcassets/app_icon_preview_teal.imageset/app_icon_preview_teal.png b/appleApp/ios/Assets.xcassets/app_icon_preview_teal.imageset/app_icon_preview_teal.png similarity index 100% rename from iosApp/flare/Assets.xcassets/app_icon_preview_teal.imageset/app_icon_preview_teal.png rename to appleApp/ios/Assets.xcassets/app_icon_preview_teal.imageset/app_icon_preview_teal.png diff --git a/iosApp/flare/Assets.xcassets/app_icon_preview_white.imageset/Contents.json b/appleApp/ios/Assets.xcassets/app_icon_preview_white.imageset/Contents.json similarity index 100% rename from iosApp/flare/Assets.xcassets/app_icon_preview_white.imageset/Contents.json rename to appleApp/ios/Assets.xcassets/app_icon_preview_white.imageset/Contents.json diff --git a/iosApp/flare/Assets.xcassets/app_icon_preview_white.imageset/app_icon_preview_white.png b/appleApp/ios/Assets.xcassets/app_icon_preview_white.imageset/app_icon_preview_white.png similarity index 100% rename from iosApp/flare/Assets.xcassets/app_icon_preview_white.imageset/app_icon_preview_white.png rename to appleApp/ios/Assets.xcassets/app_icon_preview_white.imageset/app_icon_preview_white.png diff --git a/iosApp/flare/Assets.xcassets/app_icon_preview_yellow.imageset/Contents.json b/appleApp/ios/Assets.xcassets/app_icon_preview_yellow.imageset/Contents.json similarity index 100% rename from iosApp/flare/Assets.xcassets/app_icon_preview_yellow.imageset/Contents.json rename to appleApp/ios/Assets.xcassets/app_icon_preview_yellow.imageset/Contents.json diff --git a/iosApp/flare/Assets.xcassets/app_icon_preview_yellow.imageset/app_icon_preview_yellow.png b/appleApp/ios/Assets.xcassets/app_icon_preview_yellow.imageset/app_icon_preview_yellow.png similarity index 100% rename from iosApp/flare/Assets.xcassets/app_icon_preview_yellow.imageset/app_icon_preview_yellow.png rename to appleApp/ios/Assets.xcassets/app_icon_preview_yellow.imageset/app_icon_preview_yellow.png diff --git a/iosApp/flare/Common/AppIconOption.swift b/appleApp/ios/Common/AppIconOption.swift similarity index 100% rename from iosApp/flare/Common/AppIconOption.swift rename to appleApp/ios/Common/AppIconOption.swift diff --git a/iosApp/flare/Common/MediaSaver.swift b/appleApp/ios/Common/MediaSaver.swift similarity index 97% rename from iosApp/flare/Common/MediaSaver.swift rename to appleApp/ios/Common/MediaSaver.swift index 763c60173d..6a2504c875 100644 --- a/iosApp/flare/Common/MediaSaver.swift +++ b/appleApp/ios/Common/MediaSaver.swift @@ -1,3 +1,4 @@ +import AppleFontAwesome import Foundation import GSPlayer import Kingfisher @@ -136,7 +137,7 @@ class MediaSaver: NSObject { Drops.show( .init( title: mediaType.title(success: success), - icon: success ? .faCircleCheck : .faCircleExclamation + icon: UIImage(fontAwesome: success ? .circleCheck : .circleExclamation) ) ) } @@ -147,7 +148,7 @@ class MediaSaver: NSObject { Drops.show( .init( title: mediaType.downloadStartedTitle, - icon: .faPhotoFilm + icon: UIImage(fontAwesome: .photoFilm) ) ) } diff --git a/iosApp/flare/Common/NetworkMonitor.swift b/appleApp/ios/Common/NetworkMonitor.swift similarity index 77% rename from iosApp/flare/Common/NetworkMonitor.swift rename to appleApp/ios/Common/NetworkMonitor.swift index f02f8e8ff1..58a467235f 100644 --- a/iosApp/flare/Common/NetworkMonitor.swift +++ b/appleApp/ios/Common/NetworkMonitor.swift @@ -2,18 +2,7 @@ import Foundation import Network @preconcurrency import Combine import SwiftUI - -enum NetworkKind: Equatable { - case wifi - case cellular - - var description: String { - switch self { - case .wifi: return "Wi-Fi" - case .cellular: return "Cellular" - } - } -} +import FlareAppleUI @MainActor final class NetworkMonitor: ObservableObject { @@ -56,17 +45,6 @@ final class NetworkMonitor: ObservableObject { } } -private struct NetworkKindKey: EnvironmentKey { - static let defaultValue: NetworkKind = .cellular -} - -extension EnvironmentValues { - var networkKind: NetworkKind { - get { self[NetworkKindKey.self] } - set { self[NetworkKindKey.self] = newValue } - } -} - struct NetworkStatusModifier: ViewModifier { @StateObject private var monitor = NetworkMonitor() diff --git a/iosApp/flare/Common/OriginalImageShareFile.swift b/appleApp/ios/Common/OriginalImageShareFile.swift similarity index 100% rename from iosApp/flare/Common/OriginalImageShareFile.swift rename to appleApp/ios/Common/OriginalImageShareFile.swift diff --git a/iosApp/flare/Common/String+Identifiable.swift b/appleApp/ios/Common/String+Identifiable.swift similarity index 100% rename from iosApp/flare/Common/String+Identifiable.swift rename to appleApp/ios/Common/String+Identifiable.swift diff --git a/iosApp/flare/FlareApp.swift b/appleApp/ios/FlareApp.swift similarity index 97% rename from iosApp/flare/FlareApp.swift rename to appleApp/ios/FlareApp.swift index a3ecd7a188..97b0a5d3ac 100644 --- a/iosApp/flare/FlareApp.swift +++ b/appleApp/ios/FlareApp.swift @@ -1,12 +1,13 @@ import SwiftUI import KotlinSharedUI +import FlareAppleCore import AVFAudio @main struct FlareApp: App { init() { configureAudioSessionForMixing() - IosSharedHelper.shared.initialize( + AppleSharedHelper.shared.initialize( inAppNotification: SwiftInAppNotification.shared, swiftFormatter: Formatter.shared, swiftPlatformTextRenderer: PlatformTextRenderer.shared, diff --git a/iosApp/flare/Info.plist b/appleApp/ios/Info.plist similarity index 100% rename from iosApp/flare/Info.plist rename to appleApp/ios/Info.plist diff --git a/iosApp/flare/Localizable.xcstrings b/appleApp/ios/Localizable.xcstrings similarity index 99% rename from iosApp/flare/Localizable.xcstrings rename to appleApp/ios/Localizable.xcstrings index 3c2ac9101c..50417e4559 100644 --- a/iosApp/flare/Localizable.xcstrings +++ b/appleApp/ios/Localizable.xcstrings @@ -1336,6 +1336,7 @@ } }, "accept_follow_request" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -7802,6 +7803,7 @@ } }, "all_rss_feeds_title" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -8182,6 +8184,7 @@ } }, "antenna_title" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -22053,6 +22056,196 @@ } } }, + "Block user" : { + "localizations" : { + "af" : { + "stringUnit" : { + "state" : "translated", + "value" : "Blokkeer" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "حظر" + } + }, + "bg" : { + "stringUnit" : { + "state" : "translated", + "value" : "Блокиране" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bloquejar" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Blokovat" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bloker" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Blockieren" + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Αποκλεισμός" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Block user" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bloquear" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Estä" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bloquer" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "חסימה" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tiltás" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Blocca" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ユーザーをブロック" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "사용자 차단" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Blokker" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Blokkeren" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zablokuj" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bloquear" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bloquear" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Blochează" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Заблокировать" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Blokiraj" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Blockera" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Engelle" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Заблокувати" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chặn" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "屏蔽用户" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "封鎖使用者" + } + } + } + }, "block_user_alert_message" : { "localizations" : { "af" : { @@ -22434,6 +22627,7 @@ } }, "block_user_with_handle %@" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -22815,6 +23009,7 @@ } }, "bluesky_login_auth_factor_token_hint" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -23005,6 +23200,7 @@ } }, "bluesky_login_oauth_button" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -23195,6 +23391,7 @@ } }, "bluesky_login_password_hint" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -23385,6 +23582,7 @@ } }, "bluesky_login_use_password_button" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -23575,6 +23773,7 @@ } }, "bluesky_login_username_hint" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -24147,6 +24346,7 @@ } }, "bluesky_notification_like" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -25101,6 +25301,7 @@ } }, "bluesky_notification_starterpackJoined" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -28141,7 +28342,198 @@ } } }, + "Bookmark" : { + "localizations" : { + "af" : { + "stringUnit" : { + "state" : "translated", + "value" : "Voeg boekmerk by" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "إضافة إشارة مرجعية" + } + }, + "bg" : { + "stringUnit" : { + "state" : "translated", + "value" : "Добави отметка" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Afegir marcador" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Přidat do záložek" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tilføj bogmærke" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lesezeichen hinzufügen" + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Προσθήκη σελιδοδείκτη" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bookmark" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Añadir marcador" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lisää kirjanmerkki" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ajouter un signet" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "הוסף סימנייה" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Könyvjelző hozzáadása" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aggiungi segnalibro" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ブックマーク" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "북마크" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Legg til bokmerke" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bladwijzer toevoegen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dodaj zakładkę" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Adicionar marcador" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Salvar favorito" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Adaugă semn de carte" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Добавить в закладки" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dodaj obeleživač" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lägg till bokmärke" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yer işareti ekle" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "У закладки" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Thêm dấu trang" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "书签" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "書籤" + } + } + } + }, "bookmark_add" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -28332,6 +28724,7 @@ } }, "bookmark_remove" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -28521,6 +28914,196 @@ } } }, + "Button row" : { + "localizations" : { + "af" : { + "stringUnit" : { + "state" : "translated", + "value" : "Knoppiery" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "صف الأزرار" + } + }, + "bg" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ред с бутони" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fila de botons" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Řádek tlačítek" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Knaprække" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Schaltflächenzeile" + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Σειρά κουμπιών" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Button row" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fila de botones" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Painikerivi" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rangée de boutons" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "שורת כפתורים" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gombsor" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Riga pulsanti" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ボタン行" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "버튼 행" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Knapperad" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Knoppenrij" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wiersz przycisków" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Linha de botões" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Linha de botões" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rând de butoane" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Строка кнопок" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ред дугмади" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Knapprad" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Düğme satırı" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Рядок кнопок" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hàng nút" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "按钮行" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "按鈕列" + } + } + } + }, "Cancel" : { "localizations" : { "af" : { @@ -28712,6 +29295,7 @@ } }, "cancel_button" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -28902,6 +29486,7 @@ } }, "channel_title" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -29738,22 +30323,22 @@ "value" : "Choisir l’ordre et la visibilité des boutons d’action du post" } }, - "hu" : { + "he" : { "stringUnit" : { "state" : "translated", - "value" : "Válaszd ki a bejegyzésművelet-gombok sorrendjét és láthatóságát" + "value" : "בחר את הסדר והנראות של כפתורי הפעולה בפוסט" } }, - "it" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Scegli ordine e visibilità dei pulsanti di azione del post" + "value" : "Válaszd ki a bejegyzésművelet-gombok sorrendjét és láthatóságát" } }, - "he" : { + "it" : { "stringUnit" : { "state" : "translated", - "value" : "בחר את הסדר והנראות של כפתורי הפעולה בפוסט" + "value" : "Scegli ordine e visibilità dei pulsanti di azione del post" } }, "ja" : { @@ -29768,16 +30353,16 @@ "value" : "게시물 동작 버튼의 순서와 표시 여부 선택" } }, - "nl" : { + "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Kies de volgorde en zichtbaarheid van actieknoppen voor berichten" + "value" : "Velg rekkefølge og synlighet for handlingsknapper på innlegg" } }, - "nb" : { + "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Velg rekkefølge og synlighet for handlingsknapper på innlegg" + "value" : "Kies de volgorde en zichtbaarheid van actieknoppen voor berichten" } }, "pl" : { @@ -29786,16 +30371,16 @@ "value" : "Wybierz kolejność i widoczność przycisków akcji wpisu" } }, - "pt-BR" : { + "pt" : { "stringUnit" : { "state" : "translated", - "value" : "Escolha a ordem e a visibilidade dos botões de ação da postagem" + "value" : "Escolha a ordem e a visibilidade dos botões de ação da publicação" } }, - "pt" : { + "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Escolha a ordem e a visibilidade dos botões de ação da publicação" + "value" : "Escolha a ordem e a visibilidade dos botões de ação da postagem" } }, "ro" : { @@ -29930,22 +30515,22 @@ "value" : "Choisir les actions affichées dans la rangée, le menu Plus ou masquées" } }, - "hu" : { + "he" : { "stringUnit" : { "state" : "translated", - "value" : "Válaszd ki, mely műveletek jelenjenek meg a sorban, a Továbbiak menüben vagy maradjanak rejtve" + "value" : "בחר אילו פעולות יופיעו בשורה, בתפריט עוד או יישארו מוסתרות" } }, - "it" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Scegli quali azioni appaiono nella riga, nel menu Altro o restano nascoste" + "value" : "Válaszd ki, mely műveletek jelenjenek meg a sorban, a Továbbiak menüben vagy maradjanak rejtve" } }, - "he" : { + "it" : { "stringUnit" : { "state" : "translated", - "value" : "בחר אילו פעולות יופיעו בשורה, בתפריט עוד או יישארו מוסתרות" + "value" : "Scegli quali azioni appaiono nella riga, nel menu Altro o restano nascoste" } }, "ja" : { @@ -29960,16 +30545,16 @@ "value" : "버튼 행, 더 보기 메뉴 또는 숨김에 표시할 동작 선택" } }, - "nl" : { + "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Kies welke acties in de rij, het Meer-menu of verborgen blijven" + "value" : "Velg hvilke handlinger som vises i raden, Mer-menyen eller holdes skjult" } }, - "nb" : { + "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Velg hvilke handlinger som vises i raden, Mer-menyen eller holdes skjult" + "value" : "Kies welke acties in de rij, het Meer-menu of verborgen blijven" } }, "pl" : { @@ -29978,16 +30563,16 @@ "value" : "Wybierz, które akcje pojawią się w wierszu, menu Więcej albo pozostaną ukryte" } }, - "pt-BR" : { + "pt" : { "stringUnit" : { "state" : "translated", - "value" : "Escolha quais ações aparecem na linha, no menu Mais ou ficam ocultas" + "value" : "Escolha que ações aparecem na linha, no menu Mais ou ficam ocultas" } }, - "pt" : { + "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Escolha que ações aparecem na linha, no menu Mais ou ficam ocultas" + "value" : "Escolha quais ações aparecem na linha, no menu Mais ou ficam ocultas" } }, "ro" : { @@ -30617,6 +31202,7 @@ } }, "comment" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -30806,6 +31392,196 @@ } } }, + "Comment" : { + "localizations" : { + "af" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kommentaar" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "تعليق" + } + }, + "bg" : { + "stringUnit" : { + "state" : "translated", + "value" : "Коментар" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Comenta" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Komentovat" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kommentar" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kommentieren" + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Σχόλιο" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Comment" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Comentario" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kommentti" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Commenter" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "תגובה" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hozzászólás" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Commenta" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "コメント" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "댓글" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kommentar" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Reactie" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Komentarz" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Comentar" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Comentar" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Comentariu" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Комментировать" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Коментариши" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kommentera" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yorum yap" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Коментувати" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bình luận" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "评论" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "留言" + } + } + } + }, "Comments" : { "comment" : "The title of the \"Comments\" tab in the gallery.", "isCommentAutoGenerated" : true @@ -33356,22 +34132,22 @@ "value" : "Personnaliser les actions" } }, - "hu" : { + "he" : { "stringUnit" : { "state" : "translated", - "value" : "Műveletek testreszabása" + "value" : "התאמה אישית של פעולות" } }, - "it" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Personalizza azioni" + "value" : "Műveletek testreszabása" } }, - "he" : { + "it" : { "stringUnit" : { "state" : "translated", - "value" : "התאמה אישית של פעולות" + "value" : "Personalizza azioni" } }, "ja" : { @@ -33386,16 +34162,16 @@ "value" : "동작 사용자화" } }, - "nl" : { + "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Acties aanpassen" + "value" : "Tilpass handlinger" } }, - "nb" : { + "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Tilpass handlinger" + "value" : "Acties aanpassen" } }, "pl" : { @@ -33404,13 +34180,13 @@ "value" : "Dostosuj akcje" } }, - "pt-BR" : { + "pt" : { "stringUnit" : { "state" : "translated", "value" : "Personalizar ações" } }, - "pt" : { + "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Personalizar ações" @@ -33663,6 +34439,7 @@ } }, "deep_link_account_picker_open_in_browser" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -33853,6 +34630,7 @@ } }, "deep_link_account_picker_title" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -40506,6 +41284,7 @@ } }, "error" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -42026,7 +42805,198 @@ } } }, + "Favorite" : { + "localizations" : { + "af" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gunsteling" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "تفضيل" + } + }, + "bg" : { + "stringUnit" : { + "state" : "translated", + "value" : "Любими" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Preferit" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Oblíbené" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Favorit" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Favorisieren" + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Αγαπημένο" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Favorite" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Favorito" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Suosikki" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Favori" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "מועדף" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kedvenc" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Preferito" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "お気に入り" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "즐겨찾기" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Favoritt" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Favoriet" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ulubione" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Favorito" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Favoritar" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Favorit" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "В избранное" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Omiljeno" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Favoritmarkera" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Favorilere ekle" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "У вибране" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yêu thích" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "收藏" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "收藏" + } + } + } + }, "Favourite" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -42407,7 +43377,198 @@ } } }, + "Fx share" : { + "localizations" : { + "af" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deel via FxEmbed" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "مشاركة عبر FxEmbed" + } + }, + "bg" : { + "stringUnit" : { + "state" : "translated", + "value" : "Споделяне чрез FxEmbed" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Compartir via FxEmbed" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sdílet přes FxEmbed" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Del via FxEmbed" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Über FxEmbed teilen" + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Κοινοποίηση μέσω FxEmbed" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fx share" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Compartir vía FxEmbed" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Jaa FxEmbedin kautta" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Partager via FxEmbed" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "שתף דרך FxEmbed" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Megosztás FxEmbed-en keresztül" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Condividi tramite FxEmbed" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fx共有" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fx 공유" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Del via FxEmbed" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Delen via FxEmbed" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Udostępnij przez FxEmbed" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Partilhar via FxEmbed" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Compartilhar via FxEmbed" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Partajează prin FxEmbed" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Поделиться через FxEmbed" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Podeli putem FxEmbed" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dela via FxEmbed" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "FxEmbed ile paylaş" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Поділитися через FxEmbed" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chia sẻ qua FxEmbed" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fx 分享" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fx 分享" + } + } + } + }, "fx_share" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -43167,6 +44328,386 @@ } } }, + "Hidden" : { + "localizations" : { + "af" : { + "stringUnit" : { + "state" : "translated", + "value" : "Versteek" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "مخفي" + } + }, + "bg" : { + "stringUnit" : { + "state" : "translated", + "value" : "Скрити" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Amagat" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skryté" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skjult" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ausgeblendet" + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Κρυφό" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hidden" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Oculto" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Piilotettu" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Masqué" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "מוסתר" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rejtett" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nascosto" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "非表示" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "숨김" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skjult" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verborgen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ukryte" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Oculto" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Oculto" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ascuns" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Скрыто" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Скривено" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dold" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gizli" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Приховано" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đã ẩn" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "隐藏" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "隱藏" + } + } + } + }, + "Hide action" : { + "localizations" : { + "af" : { + "stringUnit" : { + "state" : "translated", + "value" : "Versteek aksie" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "إخفاء الإجراء" + } + }, + "bg" : { + "stringUnit" : { + "state" : "translated", + "value" : "Скриване на действието" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Amaga l’acció" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skrýt akci" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skjul handling" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktion ausblenden" + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Απόκρυψη ενέργειας" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hide action" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ocultar acción" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Piilota toiminto" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Masquer l’action" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "הסתר פעולה" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Művelet elrejtése" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nascondi azione" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アクションを非表示" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "동작 숨기기" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skjul handling" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Actie verbergen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ukryj akcję" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ocultar ação" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ocultar ação" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ascunde acțiunea" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Скрыть действие" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сакриј радњу" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dölj åtgärd" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Eylemi gizle" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Приховати дію" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ẩn hành động" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "隐藏操作" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "隱藏操作" + } + } + } + }, "High" : { "localizations" : { "af" : { @@ -43358,6 +44899,7 @@ } }, "home_tab_bookmarks_title" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -43738,6 +45280,7 @@ } }, "home_tab_favorite_title" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -43928,6 +45471,7 @@ } }, "home_tab_featured_title" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -44118,6 +45662,7 @@ } }, "home_tab_feeds_title" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -44498,6 +46043,7 @@ } }, "home_tab_list_title" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -44688,6 +46234,7 @@ } }, "home_tab_me_title" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -45451,7 +46998,7 @@ }, "illustrations_title" : { "comment" : "Title of the illustrations tab.", - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -47166,6 +48713,7 @@ } }, "like" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -47355,24 +48903,24 @@ } } }, - "liked_tab_title" : { + "Like" : { "localizations" : { "af" : { "stringUnit" : { "state" : "translated", - "value" : "Gunstelinge" + "value" : "Hou van" } }, "ar" : { "stringUnit" : { "state" : "translated", - "value" : "المعجب بها" + "value" : "إعجاب" } }, "bg" : { "stringUnit" : { "state" : "translated", - "value" : "Харесани" + "value" : "Харесване" } }, "ca" : { @@ -47408,7 +48956,7 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "Liked" + "value" : "Like" } }, "es" : { @@ -47420,7 +48968,7 @@ "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Tykätyt" + "value" : "Tykkää" } }, "fr" : { @@ -47438,7 +48986,7 @@ "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Kedvelések" + "value" : "Kedvelés" } }, "it" : { @@ -47462,79 +49010,79 @@ "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Likte" + "value" : "Lik" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Vind-ik-leuks" + "value" : "Vind ik leuk" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Polubienia" + "value" : "Lubię to!" } }, "pt" : { "stringUnit" : { "state" : "translated", - "value" : "Gostos" + "value" : "Gostar" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Curtidas" + "value" : "Curtir" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Aprecieri" + "value" : "Apreciază" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Понравилось" + "value" : "Лайк" } }, "sr" : { "stringUnit" : { "state" : "translated", - "value" : "Свиђања" + "value" : "Sviđa mi se" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Gilla-markeringar" + "value" : "Gilla" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Beğeniler" + "value" : "Beğen" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Сподобалося" + "value" : "Подобається" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Đã thích" + "value" : "Thích" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "喜欢" + "value" : "赞" } }, "zh-Hant" : { @@ -47545,387 +49093,578 @@ } } }, - "list_description" : { - "localizations" : { - "af" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lys beskrywing" - } - }, - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "وصف القائمة" - } - }, - "bg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Описание на списъка" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Descripció de la llista" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Popis seznamu" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Listebeskrivelse" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Listenbeschreibung" - } - }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "Περιγραφή λίστας" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "List description" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Descripción de la lista" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Listan kuvaus" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Description de la liste" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "תיאור רשימה" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lista leírása" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Descrizione lista" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "リストの説明" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "리스트 설명" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Liste beskrivelse" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lijstbeschrijving" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Opis listy" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Descrição da lista" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Descrição da lista" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Descriere listă" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Описание списка" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Опис листе" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Listbeskrivning" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Liste açıklaması" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Опис списку" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mô tả danh sách" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "列表描述" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "列表描述" - } - } - } - }, - "list_edit_avatar" : { + "liked_tab_title" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { "state" : "translated", - "value" : "Verander avatar" + "value" : "Gunstelinge" } }, "ar" : { "stringUnit" : { "state" : "translated", - "value" : "تغيير الصورة الشخصية" + "value" : "المعجب بها" } }, "bg" : { "stringUnit" : { "state" : "translated", - "value" : "Промяна на аватара" + "value" : "Харесани" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Canvia l'avatar" + "value" : "M'agrada" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Změnit avatar" + "value" : "To se mi líbí" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Skift avatar" + "value" : "Synes godt om" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Avatar ändern" + "value" : "Gefällt mir" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Αλλαγή άβαταρ" + "value" : "Μου αρέσει" } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "List icon" + "value" : "Liked" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Cambiar avatar" + "value" : "Me gusta" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Vaihda profiilikuva" + "value" : "Tykätyt" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Changer l'avatar" + "value" : "J'aime" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "שנה אוואטר" + "value" : "אהבתי" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Profilkép módosítása" + "value" : "Kedvelések" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Cambia avatar" + "value" : "Mi piace" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "アバターを変更" + "value" : "いいね" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "아바타 변경" + "value" : "좋아요" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Liste ikon" + "value" : "Likte" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Avatar wijzigen" + "value" : "Vind-ik-leuks" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Zmień awatar" + "value" : "Polubienia" } }, "pt" : { "stringUnit" : { "state" : "translated", - "value" : "Alterar avatar" + "value" : "Gostos" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Alterar avatar" + "value" : "Curtidas" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Schimbă avatarul" + "value" : "Aprecieri" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Изменить аватар" + "value" : "Понравилось" } }, "sr" : { "stringUnit" : { "state" : "translated", - "value" : "Промени аватар" + "value" : "Свиђања" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Ändra avatar" + "value" : "Gilla-markeringar" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Avatarı değiştir" + "value" : "Beğeniler" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Змінити аватар" + "value" : "Сподобалося" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Thay đổi hình đại diện" + "value" : "Đã thích" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "更改头像" + "value" : "喜欢" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "更改頭像" + "value" : "喜歡" } } } }, - "list_edit_description" : { + "list_description" : { + "localizations" : { + "af" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lys beskrywing" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "وصف القائمة" + } + }, + "bg" : { + "stringUnit" : { + "state" : "translated", + "value" : "Описание на списъка" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Descripció de la llista" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Popis seznamu" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Listebeskrivelse" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Listenbeschreibung" + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Περιγραφή λίστας" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "List description" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Descripción de la lista" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Listan kuvaus" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Description de la liste" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "תיאור רשימה" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lista leírása" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Descrizione lista" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "リストの説明" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "리스트 설명" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Liste beskrivelse" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lijstbeschrijving" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Opis listy" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Descrição da lista" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Descrição da lista" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Descriere listă" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Описание списка" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Опис листе" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Listbeskrivning" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Liste açıklaması" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Опис списку" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mô tả danh sách" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "列表描述" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "列表描述" + } + } + } + }, + "list_edit_avatar" : { + "localizations" : { + "af" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verander avatar" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "تغيير الصورة الشخصية" + } + }, + "bg" : { + "stringUnit" : { + "state" : "translated", + "value" : "Промяна на аватара" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Canvia l'avatar" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Změnit avatar" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skift avatar" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Avatar ändern" + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Αλλαγή άβαταρ" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "List icon" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cambiar avatar" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vaihda profiilikuva" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Changer l'avatar" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "שנה אוואטר" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Profilkép módosítása" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cambia avatar" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アバターを変更" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "아바타 변경" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Liste ikon" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Avatar wijzigen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zmień awatar" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alterar avatar" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alterar avatar" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Schimbă avatarul" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Изменить аватар" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Промени аватар" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ändra avatar" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Avatarı değiştir" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Змінити аватар" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Thay đổi hình đại diện" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "更改头像" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "更改頭像" + } + } + } + }, + "list_edit_description" : { "localizations" : { "af" : { "stringUnit" : { @@ -54387,6 +56126,7 @@ } }, "login_button" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -55338,7 +57078,7 @@ }, "manga_title" : { "comment" : "Title of the manga tab.", - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -56099,6 +57839,7 @@ } }, "mastodon_item_pinned" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -56859,6 +58600,7 @@ } }, "mastodon_notification_favourite" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -57049,6 +58791,7 @@ } }, "mastodon_notification_follow" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -57239,6 +58982,7 @@ } }, "mastodon_notification_follow_request" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -57429,6 +59173,7 @@ } }, "mastodon_notification_mention" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -57619,6 +59364,7 @@ } }, "mastodon_notification_poll" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -57809,6 +59555,7 @@ } }, "mastodon_notification_reblog" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -57999,6 +59746,7 @@ } }, "mastodon_notification_status" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -58189,6 +59937,7 @@ } }, "mastodon_notification_update" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -58759,6 +60508,7 @@ } }, "mastodon_tab_local_title" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -58949,6 +60699,7 @@ } }, "mastodon_tab_public_title" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -59329,6 +61080,7 @@ } }, "matrix_followers" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -59519,6 +61271,7 @@ } }, "matrix_following" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -59710,7 +61463,7 @@ }, "media_title" : { "comment" : "Title of the media tab.", - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -94272,6 +96025,7 @@ } }, "misskey_notification_achievement_earned" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -94462,6 +96216,7 @@ } }, "misskey_notification_achievement_earned %@ %@" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -94652,6 +96407,7 @@ } }, "misskey_notification_app" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -94842,6 +96598,7 @@ } }, "misskey_notification_chat_room_invitation_received" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -95032,6 +96789,7 @@ } }, "misskey_notification_create_token" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -95222,6 +96980,7 @@ } }, "misskey_notification_export_completed" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -95603,6 +97362,7 @@ } }, "misskey_notification_follow_request_accepted" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -95793,6 +97553,7 @@ } }, "misskey_notification_login" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -96365,6 +98126,7 @@ } }, "misskey_notification_quote" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -96555,6 +98317,7 @@ } }, "misskey_notification_reaction" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -97127,6 +98890,7 @@ } }, "misskey_notification_reply" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -97317,6 +99081,7 @@ } }, "misskey_notification_role_assigned" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -97507,6 +99272,7 @@ } }, "misskey_notification_scheduled_note_post_failed" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -97697,6 +99463,7 @@ } }, "misskey_notification_scheduled_note_posted" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -97887,6 +99654,7 @@ } }, "misskey_notification_test" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -98838,6 +100606,7 @@ } }, "mixed_timeline_title" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -99218,6 +100987,7 @@ } }, "more" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -99598,6 +101368,196 @@ } } }, + "More menu" : { + "localizations" : { + "af" : { + "stringUnit" : { + "state" : "translated", + "value" : "Meer-kieslys" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "قائمة المزيد" + } + }, + "bg" : { + "stringUnit" : { + "state" : "translated", + "value" : "Меню „Още“" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Menú Més" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nabídka Další" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Menuen Mere" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mehr-Menü" + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Μενού Περισσότερα" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "More menu" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Menú Más" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lisää-valikko" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Menu Plus" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "תפריט עוד" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Továbbiak menü" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Menu Altro" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "その他メニュー" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "더 보기 메뉴" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mer-meny" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Meer-menu" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Menu Więcej" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Menu Mais" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Menu Mais" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Meniu Mai multe" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Меню Ещё" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Мени Још" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Menyn Mer" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Daha Fazla menüsü" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Меню Ще" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Menu Thêm" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "更多菜单" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "更多選單" + } + } + } + }, "Move down" : { "comment" : "A button that moves a post action family down in the list.", "isCommentAutoGenerated" : true, @@ -99674,22 +101634,22 @@ "value" : "Descendre" } }, - "hu" : { + "he" : { "stringUnit" : { "state" : "translated", - "value" : "Mozgatás le" + "value" : "העבר למטה" } }, - "it" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Sposta giù" + "value" : "Mozgatás le" } }, - "he" : { + "it" : { "stringUnit" : { "state" : "translated", - "value" : "העבר למטה" + "value" : "Sposta giù" } }, "ja" : { @@ -99704,16 +101664,16 @@ "value" : "아래로 이동" } }, - "nl" : { + "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Omlaag verplaatsen" + "value" : "Flytt ned" } }, - "nb" : { + "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Flytt ned" + "value" : "Omlaag verplaatsen" } }, "pl" : { @@ -99722,13 +101682,13 @@ "value" : "Przenieś w dół" } }, - "pt-BR" : { + "pt" : { "stringUnit" : { "state" : "translated", "value" : "Mover para baixo" } }, - "pt" : { + "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Mover para baixo" @@ -99790,6 +101750,386 @@ } } }, + "Move to button row" : { + "localizations" : { + "af" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skuif na knoppiery" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "نقل إلى صف الأزرار" + } + }, + "bg" : { + "stringUnit" : { + "state" : "translated", + "value" : "Преместване в реда с бутони" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mou a la fila de botons" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Přesunout do řádku tlačítek" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Flyt til knaprække" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "In die Schaltflächenzeile verschieben" + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Μετακίνηση στη σειρά κουμπιών" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Move to button row" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mover a la fila de botones" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Siirrä painikeriville" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Déplacer vers la rangée de boutons" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "העבר לשורת הכפתורים" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Áthelyezés a gombsorba" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sposta nella riga pulsanti" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ボタン行へ移動" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "버튼 행으로 이동" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Flytt til knapperad" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Naar knoppenrij verplaatsen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Przenieś do wiersza przycisków" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mover para a linha de botões" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mover para a linha de botões" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mută în rândul de butoane" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Переместить в строку кнопок" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Премести у ред дугмади" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Flytta till knapprad" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Düğme satırına taşı" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Перемістити до рядка кнопок" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chuyển vào hàng nút" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "移动到按钮行" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "移至按鈕列" + } + } + } + }, + "Move to More menu" : { + "localizations" : { + "af" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skuif na Meer-kieslys" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "نقل إلى قائمة المزيد" + } + }, + "bg" : { + "stringUnit" : { + "state" : "translated", + "value" : "Преместване в менюто „Още“" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mou al menú Més" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Přesunout do nabídky Další" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Flyt til menuen Mere" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ins Mehr-Menü verschieben" + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Μετακίνηση στο μενού Περισσότερα" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Move to More menu" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mover al menú Más" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Siirrä Lisää-valikkoon" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Déplacer vers le menu Plus" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "העבר לתפריט עוד" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Áthelyezés a Továbbiak menübe" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sposta nel menu Altro" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "その他メニューへ移動" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "더 보기 메뉴로 이동" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Flytt til Mer-meny" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Naar Meer-menu verplaatsen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Przenieś do menu Więcej" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mover para o menu Mais" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mover para o menu Mais" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mută în meniul Mai multe" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Переместить в меню Ещё" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Премести у мени Још" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Flytta till menyn Mer" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Daha Fazla menüsüne taşı" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Перемістити до меню Ще" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chuyển vào menu Thêm" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "移动到更多菜单" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "移至更多選單" + } + } + } + }, "Move up" : { "comment" : "A button that moves a post action up in the list.", "isCommentAutoGenerated" : true, @@ -99866,22 +102206,22 @@ "value" : "Monter" } }, - "hu" : { + "he" : { "stringUnit" : { "state" : "translated", - "value" : "Mozgatás fel" + "value" : "העבר למעלה" } }, - "it" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Sposta su" + "value" : "Mozgatás fel" } }, - "he" : { + "it" : { "stringUnit" : { "state" : "translated", - "value" : "העבר למעלה" + "value" : "Sposta su" } }, "ja" : { @@ -99896,16 +102236,16 @@ "value" : "위로 이동" } }, - "nl" : { + "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Omhoog verplaatsen" + "value" : "Flytt opp" } }, - "nb" : { + "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Flytt opp" + "value" : "Omhoog verplaatsen" } }, "pl" : { @@ -99914,13 +102254,13 @@ "value" : "Przenieś w górę" } }, - "pt-BR" : { + "pt" : { "stringUnit" : { "state" : "translated", "value" : "Mover para cima" } }, - "pt" : { + "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Mover para cima" @@ -100172,6 +102512,196 @@ } } }, + "Mute user" : { + "localizations" : { + "af" : { + "stringUnit" : { + "state" : "translated", + "value" : "Demp" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "كتم" + } + }, + "bg" : { + "stringUnit" : { + "state" : "translated", + "value" : "Заглушаване" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Silenciar" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skrýt" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lydløs" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Stummschalten" + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Σίγαση" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mute user" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Silenciar" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mykistä" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Masquer" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "השתקה" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Némítás" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Silenzia" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ユーザーをミュート" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "사용자 뮤트" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Demp" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dempen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wycisz" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Silenciar" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Silenciar" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ignoră" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Игнорировать" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Utišaj" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tysta" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sessize al" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Приглушити" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ẩn" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "静音用户" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "靜音使用者" + } + } + } + }, "mute_user_alert_message" : { "localizations" : { "af" : { @@ -100553,6 +103083,7 @@ } }, "mute_user_with_handle %@" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -101009,22 +103540,22 @@ "value" : "Aucune action" } }, - "hu" : { + "he" : { "stringUnit" : { "state" : "translated", - "value" : "Nincsenek műveletek" + "value" : "אין פעולות" } }, - "it" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Nessuna azione" + "value" : "Nincsenek műveletek" } }, - "he" : { + "it" : { "stringUnit" : { "state" : "translated", - "value" : "אין פעולות" + "value" : "Nessuna azione" } }, "ja" : { @@ -101039,16 +103570,16 @@ "value" : "동작 없음" } }, - "nl" : { + "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Geen acties" + "value" : "Ingen handlinger" } }, - "nb" : { + "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Ingen handlinger" + "value" : "Geen acties" } }, "pl" : { @@ -101057,16 +103588,16 @@ "value" : "Brak akcji" } }, - "pt-BR" : { + "pt" : { "stringUnit" : { "state" : "translated", - "value" : "Nenhuma ação" + "value" : "Sem ações" } }, - "pt" : { + "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Sem ações" + "value" : "Nenhuma ação" } }, "ro" : { @@ -101697,6 +104228,7 @@ } }, "nostr_login_account_hint" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -101887,6 +104419,7 @@ } }, "nostr_login_amber_button" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -102077,6 +104610,7 @@ } }, "nostr_login_qr_button" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -102837,6 +105371,7 @@ } }, "nostr_login_title" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -107111,7 +109646,7 @@ }, "pixiv_ranking_day_female_title" : { "comment" : "Title of the ranking of female artists on the pixiv app.", - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -107303,7 +109838,7 @@ }, "pixiv_ranking_day_male_title" : { "comment" : "Title of the ranking of the most popular works of artists who are male.", - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -107495,7 +110030,7 @@ }, "pixiv_ranking_day_manga_title" : { "comment" : "Title of the ranking of manga created on the same day.", - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -107687,7 +110222,7 @@ }, "pixiv_ranking_month_title" : { "comment" : "Title of the monthly ranking of the pixiv ranking.", - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -107879,7 +110414,7 @@ }, "pixiv_ranking_week_original_title" : { "comment" : "Title of the ranking of the week of original works.", - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -108071,7 +110606,7 @@ }, "pixiv_ranking_week_rookie_title" : { "comment" : "Title of the ranking of the week for the \"Rookie\" ranking on the Pixiv app.", - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -108263,7 +110798,7 @@ }, "pixiv_ranking_week_title" : { "comment" : "Title of the weekly ranking of the Pixiv ranking.", - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -109480,22 +112015,22 @@ "value" : "Actions du post" } }, - "hu" : { + "he" : { "stringUnit" : { "state" : "translated", - "value" : "Bejegyzésműveletek" + "value" : "פעולות פוסט" } }, - "it" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Azioni del post" + "value" : "Bejegyzésműveletek" } }, - "he" : { + "it" : { "stringUnit" : { "state" : "translated", - "value" : "פעולות פוסט" + "value" : "Azioni del post" } }, "ja" : { @@ -109510,16 +112045,16 @@ "value" : "게시물 동작" } }, - "nl" : { + "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Berichtacties" + "value" : "Innleggshandlinger" } }, - "nb" : { + "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Innleggshandlinger" + "value" : "Berichtacties" } }, "pl" : { @@ -109528,16 +112063,16 @@ "value" : "Akcje wpisu" } }, - "pt-BR" : { + "pt" : { "stringUnit" : { "state" : "translated", - "value" : "Ações da postagem" + "value" : "Ações da publicação" } }, - "pt" : { + "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Ações da publicação" + "value" : "Ações da postagem" } }, "ro" : { @@ -109597,6 +112132,7 @@ } }, "posts_title" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -109788,7 +112324,7 @@ }, "posts_with_replies_title" : { "comment" : "Title of a tab that shows posts and replies.", - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -111191,6 +113727,7 @@ } }, "quote" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -111380,7 +113917,388 @@ } } }, + "Quote" : { + "localizations" : { + "af" : { + "stringUnit" : { + "state" : "translated", + "value" : "Haal aan" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "اقتباس" + } + }, + "bg" : { + "stringUnit" : { + "state" : "translated", + "value" : "Цитат" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Citar" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Citovat" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Citér" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zitieren" + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Παράθεση" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Quote" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Citar" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lainaa" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Citer" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "ציטוט" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Idézés" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cita" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "引用" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "인용" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Siter" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Citeren" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cytat" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Citar" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Citar" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Citat" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Цитировать" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Citiraj" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Citera" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alıntıla" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Цитата" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trích dẫn" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "引用" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "引用" + } + } + } + }, + "React" : { + "localizations" : { + "af" : { + "stringUnit" : { + "state" : "translated", + "value" : "Voeg reaksie by" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "إضافة تفاعل" + } + }, + "bg" : { + "stringUnit" : { + "state" : "translated", + "value" : "Добави реакция" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Afegir reacció" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Přidat reakci" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tilføj reaktion" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Reaktion hinzufügen" + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Προσθήκη αντίδρασης" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "React" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Añadir reacción" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lisää reaktio" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ajouter une réaction" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "הוסף תגובה" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Reakció hozzáadása" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aggiungi reazione" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "リアクション" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "반응" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Legg til reaksjon" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Reactie toevoegen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dodaj reakcję" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Adicionar reação" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Adicionar reação" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Adaugă reacție" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Добавить реакцию" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dodaj reakciju" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lägg till reaktion" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tepki ekle" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Додати реакцію" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Thêm cảm xúc" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "回应" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "反應" + } + } + } + }, "reaction_add" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -111571,6 +114489,7 @@ } }, "reaction_remove" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -111959,6 +114878,7 @@ "isCommentAutoGenerated" : true }, "reject_follow_request" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -112149,6 +115069,7 @@ } }, "relation_blocked" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -112339,6 +115260,7 @@ } }, "relation_follow" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -112529,6 +115451,7 @@ } }, "relation_following" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -112719,6 +115642,7 @@ } }, "relation_is_fans" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -112909,6 +115833,7 @@ } }, "relation_request_follow" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -113099,6 +116024,7 @@ } }, "relation_requested" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -113289,6 +116215,7 @@ } }, "reply" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -113478,778 +116405,778 @@ } } }, - "Reply to %@" : { + "Reply" : { "localizations" : { "af" : { "stringUnit" : { "state" : "translated", - "value" : "Antwoord vir %@" + "value" : "Antwoord" } }, "ar" : { "stringUnit" : { "state" : "translated", - "value" : "الرد على %@" + "value" : "رد" } }, "bg" : { "stringUnit" : { "state" : "translated", - "value" : "Отговор на %@" + "value" : "Отговор" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Respon a %@" + "value" : "Respondre" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Odpovědět %@" + "value" : "Odpovědět" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Svar til %@" + "value" : "Svar" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "An %@ antworten" + "value" : "Antworten" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Απάντηση στον %@" + "value" : "Απάντηση" } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Reply to %@" + "value" : "Reply" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Responder a %@" + "value" : "Responder" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Vastaa %@" + "value" : "Vastaa" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Répondre à %@" + "value" : "Répondre" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "הגב ל-%@" + "value" : "תגובה" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Válasz neki: %@" + "value" : "Válasz" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Rispondi a %@" + "value" : "Rispondi" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "%@へ返信" + "value" : "返信" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "%@에게 답장" + "value" : "답글" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Svar til %@" + "value" : "Svar" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Antwoord aan %@" + "value" : "Beantwoorden" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Odpowiedz %@" + "value" : "Odpowiedz" } }, "pt" : { "stringUnit" : { "state" : "translated", - "value" : "Responder a %@" + "value" : "Responder" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Responder a %@" + "value" : "Responder" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Răspunde lui %@" + "value" : "Răspunde" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Ответить %@" + "value" : "Ответить" } }, "sr" : { "stringUnit" : { "state" : "translated", - "value" : "Одговори кориснику %@" + "value" : "Odgovori" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Svara till %@" + "value" : "Svara" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "%@ kişisine yanıt ver" + "value" : "Yanıtla" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Відповісти %@" + "value" : "Відповісти" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Trả lời %@" + "value" : "Trả lời" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "回复到 %@" + "value" : "回复" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "回覆 %@" + "value" : "回覆" } } } }, - "report" : { + "Reply to %@" : { "localizations" : { "af" : { "stringUnit" : { "state" : "translated", - "value" : "verslag" + "value" : "Antwoord vir %@" } }, "ar" : { "stringUnit" : { "state" : "translated", - "value" : "تقرير" + "value" : "الرد على %@" } }, "bg" : { "stringUnit" : { "state" : "translated", - "value" : "Докладване" + "value" : "Отговор на %@" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Informar" + "value" : "Respon a %@" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Nahlásit" + "value" : "Odpovědět %@" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Anmeld" + "value" : "Svar til %@" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Bericht" + "value" : "An %@ antworten" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Αναφορά" + "value" : "Απάντηση στον %@" } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Report" + "value" : "Reply to %@" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Reportar" + "value" : "Responder a %@" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Ilmianna" + "value" : "Vastaa %@" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Rapporter" + "value" : "Répondre à %@" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "דיווח" + "value" : "הגב ל-%@" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Jelentés" + "value" : "Válasz neki: %@" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Segnala" + "value" : "Rispondi a %@" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "レポート" + "value" : "%@へ返信" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "신고" + "value" : "%@에게 답장" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Rapporter" + "value" : "Svar til %@" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Rapporteren" + "value" : "Antwoord aan %@" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Zgłoszenie" + "value" : "Odpowiedz %@" } }, "pt" : { "stringUnit" : { "state" : "translated", - "value" : "Denunciar" + "value" : "Responder a %@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Denunciar" + "value" : "Responder a %@" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Raport" + "value" : "Răspunde lui %@" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Отчет" + "value" : "Ответить %@" } }, "sr" : { "stringUnit" : { "state" : "translated", - "value" : "Prijavi" + "value" : "Одговори кориснику %@" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Rapportera" + "value" : "Svara till %@" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Bildir" + "value" : "%@ kişisine yanıt ver" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Доповісти" + "value" : "Відповісти %@" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Báo cáo" + "value" : "Trả lời %@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "举报" + "value" : "回复到 %@" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "舉報" + "value" : "回覆 %@" } } } }, - "Retry" : { + "report" : { "localizations" : { "af" : { "stringUnit" : { "state" : "translated", - "value" : "Probeer weer" + "value" : "verslag" } }, "ar" : { "stringUnit" : { "state" : "translated", - "value" : "إعادة المحاولة" + "value" : "تقرير" } }, "bg" : { "stringUnit" : { "state" : "translated", - "value" : "Повторен опит" + "value" : "Докладване" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Reintenteu" + "value" : "Informar" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Opakovat" + "value" : "Nahlásit" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Prøv igen" + "value" : "Anmeld" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Wiederholen" + "value" : "Bericht" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Επαναλήψη" + "value" : "Αναφορά" } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Retry" + "value" : "Report" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Reintentar" + "value" : "Reportar" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Yritä uudelleen" + "value" : "Ilmianna" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Réessayer" + "value" : "Rapporter" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "ניסיון חוזר" + "value" : "דיווח" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Újra" + "value" : "Jelentés" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Riprova" + "value" : "Segnala" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "再試行" + "value" : "レポート" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "재시도" + "value" : "신고" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Prøv igjen" + "value" : "Rapporter" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Opnieuw proberen" + "value" : "Rapporteren" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Spróbuj ponownie" + "value" : "Zgłoszenie" } }, "pt" : { "stringUnit" : { "state" : "translated", - "value" : "Repetir" + "value" : "Denunciar" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Repetir" + "value" : "Denunciar" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Reîncearcă" + "value" : "Raport" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Повторить" + "value" : "Отчет" } }, "sr" : { "stringUnit" : { "state" : "translated", - "value" : "Pokušaj ponovo" + "value" : "Prijavi" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Försök igen" + "value" : "Rapportera" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Tekrar dene" + "value" : "Bildir" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Повторити" + "value" : "Доповісти" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Thử lại" + "value" : "Báo cáo" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "重试" + "value" : "举报" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "重試" + "value" : "舉報" } } } }, - "Retry translation" : { + "Report" : { "localizations" : { "af" : { "stringUnit" : { "state" : "translated", - "value" : "Probeer vertaling weer" + "value" : "Rapporteer" } }, "ar" : { "stringUnit" : { "state" : "translated", - "value" : "إعادة محاولة الترجمة" + "value" : "إبلاغ" } }, "bg" : { "stringUnit" : { "state" : "translated", - "value" : "Повторен опит за превод" + "value" : "Докладване" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Torna a provar la traducció" + "value" : "Informar" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Zkusit překlad znovu" + "value" : "Nahlásit" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Prøv oversættelse igen" + "value" : "Anmeld" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Übersetzung wiederholen" + "value" : "Melden" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Επανάληψη μετάφρασης" + "value" : "Αναφορά" } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Retry translation" + "value" : "Report" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Reintentar traducción" + "value" : "Reportar" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Yritä kääntämistä uudelleen" + "value" : "Raportoi" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Réessayer la traduction" + "value" : "Signaler" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "נסה שוב תרגום" + "value" : "דיווח" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Fordítás újbóli megkérése" + "value" : "Jelentés" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Riprova traduzione" + "value" : "Segnala" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "翻訳を再試行" + "value" : "報告" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "번역 재시도" + "value" : "신고" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Prøv igjen oversettelse" + "value" : "Rapporter" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Vertaling opnieuw proberen" + "value" : "Rapporteren" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Ponów tłumaczenie" + "value" : "Zgłoś" } }, "pt" : { "stringUnit" : { "state" : "translated", - "value" : "Repetir tradução" + "value" : "Denunciar" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Repetir tradução" + "value" : "Denunciar" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Reîncearcă traducerea" + "value" : "Raportează" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Повторить перевод" + "value" : "Пожаловаться" } }, "sr" : { "stringUnit" : { "state" : "translated", - "value" : "Понови превод" + "value" : "Prijavi" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Försök igen att översätta" + "value" : "Rapportera" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Çeviriyi yeniden dene" + "value" : "Bildir" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Повторити переклад" + "value" : "Скаржитися" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Thử lại bản dịch" + "value" : "Báo cáo" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "重试翻译" + "value" : "举报" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "重試翻譯" + "value" : "檢舉" } } } }, - "retweet" : { + "Repost" : { "localizations" : { "af" : { "stringUnit" : { "state" : "translated", - "value" : "her-twiet" + "value" : "Herplaas" } }, "ar" : { "stringUnit" : { "state" : "translated", - "value" : "تغريدة" + "value" : "إعادة نشر" } }, "bg" : { @@ -114261,25 +117188,25 @@ "ca" : { "stringUnit" : { "state" : "translated", - "value" : "retuitejar" + "value" : "Republicar" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Opakovat" + "value" : "Přesdílet" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Retweet" + "value" : "Post igen" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Retweet" + "value" : "Teilen" } }, "el" : { @@ -114297,13 +117224,13 @@ "es" : { "stringUnit" : { "state" : "translated", - "value" : "Republicar" + "value" : "Compartir" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Uudelleenjulkaisu" + "value" : "Jaa" } }, "fr" : { @@ -114321,7 +117248,7 @@ "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Újra közzététel" + "value" : "Újraközlés" } }, "it" : { @@ -114333,25 +117260,597 @@ "ja" : { "stringUnit" : { "state" : "translated", - "value" : "再投稿" + "value" : "リポスト" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "재게시" + "value" : "리포스트" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Gjenkjenn" + "value" : "Reposter" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Retweet" + "value" : "Delen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Podaj dalej" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Republicar" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Repostar" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Repostare" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Репост" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ponovo objavi" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Reposta" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yeniden paylaş" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Поширити" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đăng lại" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "转发" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "轉發" + } + } + } + }, + "Retry" : { + "localizations" : { + "af" : { + "stringUnit" : { + "state" : "translated", + "value" : "Probeer weer" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "إعادة المحاولة" + } + }, + "bg" : { + "stringUnit" : { + "state" : "translated", + "value" : "Повторен опит" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Reintenteu" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Opakovat" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Prøv igen" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wiederholen" + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Επαναλήψη" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Retry" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Reintentar" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yritä uudelleen" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Réessayer" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "ניסיון חוזר" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Újra" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Riprova" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "再試行" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "재시도" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Prøv igjen" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Opnieuw proberen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Spróbuj ponownie" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Repetir" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Repetir" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Reîncearcă" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Повторить" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pokušaj ponovo" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Försök igen" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tekrar dene" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Повторити" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Thử lại" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "重试" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "重試" + } + } + } + }, + "Retry translation" : { + "extractionState" : "stale", + "localizations" : { + "af" : { + "stringUnit" : { + "state" : "translated", + "value" : "Probeer vertaling weer" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "إعادة محاولة الترجمة" + } + }, + "bg" : { + "stringUnit" : { + "state" : "translated", + "value" : "Повторен опит за превод" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Torna a provar la traducció" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zkusit překlad znovu" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Prøv oversættelse igen" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Übersetzung wiederholen" + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Επανάληψη μετάφρασης" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Retry translation" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Reintentar traducción" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yritä kääntämistä uudelleen" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Réessayer la traduction" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "נסה שוב תרגום" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fordítás újbóli megkérése" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Riprova traduzione" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "翻訳を再試行" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "번역 재시도" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Prøv igjen oversettelse" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vertaling opnieuw proberen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ponów tłumaczenie" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Repetir tradução" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Repetir tradução" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Reîncearcă traducerea" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Повторить перевод" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Понови превод" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Försök igen att översätta" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Çeviriyi yeniden dene" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Повторити переклад" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Thử lại bản dịch" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "重试翻译" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "重試翻譯" + } + } + } + }, + "retweet" : { + "extractionState" : "stale", + "localizations" : { + "af" : { + "stringUnit" : { + "state" : "translated", + "value" : "her-twiet" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "تغريدة" + } + }, + "bg" : { + "stringUnit" : { + "state" : "translated", + "value" : "Препубликуване" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "retuitejar" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Opakovat" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Retweet" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Retweet" + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Αναδημοσίευση" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Repost" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Republicar" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uudelleenjulkaisu" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Repartager" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "פרסום מחדש" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Újra közzététel" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ripubblica" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "再投稿" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "재게시" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gjenkjenn" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Retweet" } }, "pl" : { @@ -114429,6 +117928,7 @@ } }, "retweet_remove" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -120891,6 +124391,7 @@ } }, "send_message" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -122222,6 +125723,7 @@ } }, "service_select_next_button" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -126974,6 +130476,7 @@ } }, "share" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -127163,6 +130666,196 @@ } } }, + "Share" : { + "localizations" : { + "af" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deel" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "مشاركة" + } + }, + "bg" : { + "stringUnit" : { + "state" : "translated", + "value" : "Споделяне" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Compartir" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sdílet" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Del" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Teilen" + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Κοινοποίηση" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Share" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Compartir" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Jaa" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Partager" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "שיתוף" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Megosztás" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Condividi" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "共有" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "공유" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Del" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Delen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Udostępnij" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Partilhar" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Compartilhar" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Partajează" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Поделиться" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Podeli" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dela" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Paylaş" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Поділитися" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chia sẻ" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "分享" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "分享" + } + } + } + }, "Share image" : { "comment" : "A title for the share sheet.", "localizations" : { @@ -128115,6 +131808,7 @@ } }, "Show original" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -128687,6 +132381,7 @@ } }, "social_title" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -129638,6 +133333,7 @@ } }, "status_insight_trace_agent_closing" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -129828,6 +133524,7 @@ } }, "status_insight_trace_agent_completed" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -130018,6 +133715,7 @@ } }, "status_insight_trace_agent_failed" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -130208,6 +133906,7 @@ } }, "status_insight_trace_agent_started" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -130398,6 +134097,7 @@ } }, "status_insight_trace_asking_model" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -130588,6 +134288,7 @@ } }, "status_insight_trace_images_unsupported_fallback" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -130778,6 +134479,7 @@ } }, "status_insight_trace_loading_post_context" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -130968,6 +134670,7 @@ } }, "status_insight_trace_model_response_received" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -131158,6 +134861,7 @@ } }, "status_insight_trace_post_context_loaded" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -131348,6 +135052,7 @@ } }, "status_insight_trace_preparing_images" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -131538,6 +135243,7 @@ } }, "status_insight_trace_running_step" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -131728,6 +135434,7 @@ } }, "status_insight_trace_step_completed" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -131918,6 +135625,7 @@ } }, "status_insight_trace_step_failed" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -132108,6 +135816,7 @@ } }, "status_insight_trace_strategy_completed" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -132298,6 +136007,7 @@ } }, "status_insight_trace_strategy_started" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -132488,6 +136198,7 @@ } }, "status_insight_trace_streaming_completed" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -132678,6 +136389,7 @@ } }, "status_insight_trace_streaming_failed" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -132868,6 +136580,7 @@ } }, "status_insight_trace_streaming_response" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -133058,6 +136771,7 @@ } }, "status_insight_trace_streaming_started" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -133248,6 +136962,7 @@ } }, "status_insight_trace_subgraph_completed" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -133438,6 +137153,7 @@ } }, "status_insight_trace_subgraph_failed" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -133628,6 +137344,7 @@ } }, "status_insight_trace_subgraph_started" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -133818,6 +137535,7 @@ } }, "status_insight_trace_tool_call_failed" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -134008,6 +137726,7 @@ } }, "status_insight_trace_tool_load_status_context_completed" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -134198,6 +137917,7 @@ } }, "status_insight_trace_tool_load_status_context_failed" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -134388,6 +138108,7 @@ } }, "status_insight_trace_tool_load_status_context_started" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -134578,6 +138299,7 @@ } }, "status_insight_trace_tool_load_status_context_validation_failed" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -134768,6 +138490,7 @@ } }, "status_insight_trace_tool_search_status_completed" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -134958,6 +138681,7 @@ } }, "status_insight_trace_tool_search_status_failed" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -135148,6 +138872,7 @@ } }, "status_insight_trace_tool_search_status_started" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -135338,6 +139063,7 @@ } }, "status_insight_trace_tool_search_status_validation_failed" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -135528,6 +139254,7 @@ } }, "status_insight_trace_tool_validation_failed" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -141230,6 +144957,7 @@ }, "tab_settings_default" : { "comment" : "Default tab icon name.", + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -148835,6 +152563,7 @@ } }, "Unfavourite" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -149025,6 +152754,7 @@ } }, "unlike" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -149595,6 +153325,7 @@ } }, "user_list_title_fans" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -149785,6 +153516,7 @@ } }, "user_list_title_following" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -149975,6 +153707,7 @@ } }, "verify_button" : { + "extractionState" : "stale", "localizations" : { "af" : { "stringUnit" : { @@ -151501,3616 +155234,6 @@ } } } - }, - "Button row" : { - "localizations" : { - "af" : { - "stringUnit" : { - "state" : "translated", - "value" : "Knoppiery" - } - }, - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "صف الأزرار" - } - }, - "bg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ред с бутони" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Fila de botons" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Řádek tlačítek" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Knaprække" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Schaltflächenzeile" - } - }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "Σειρά κουμπιών" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Button row" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Fila de botones" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Painikerivi" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Rangée de boutons" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Gombsor" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Riga pulsanti" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "שורת כפתורים" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "ボタン行" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "버튼 행" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Knoppenrij" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Knapperad" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Wiersz przycisków" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Linha de botões" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Linha de botões" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Rând de butoane" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Строка кнопок" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ред дугмади" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Knapprad" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Düğme satırı" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Рядок кнопок" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Hàng nút" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "按钮行" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "按鈕列" - } - } - } - }, - "More menu" : { - "localizations" : { - "af" : { - "stringUnit" : { - "state" : "translated", - "value" : "Meer-kieslys" - } - }, - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "قائمة المزيد" - } - }, - "bg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Меню „Още“" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Menú Més" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nabídka Další" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Menuen Mere" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mehr-Menü" - } - }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "Μενού Περισσότερα" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "More menu" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Menú Más" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lisää-valikko" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Menu Plus" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Továbbiak menü" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Menu Altro" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "תפריט עוד" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "その他メニュー" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "더 보기 메뉴" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Meer-menu" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mer-meny" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Menu Więcej" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Menu Mais" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Menu Mais" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Meniu Mai multe" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Меню Ещё" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Мени Још" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Menyn Mer" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Daha Fazla menüsü" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Меню Ще" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Menu Thêm" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "更多菜单" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "更多選單" - } - } - } - }, - "Hidden" : { - "localizations" : { - "af" : { - "stringUnit" : { - "state" : "translated", - "value" : "Versteek" - } - }, - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "مخفي" - } - }, - "bg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Скрити" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Amagat" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Skryté" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Skjult" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ausgeblendet" - } - }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "Κρυφό" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Hidden" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Oculto" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Piilotettu" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Masqué" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Rejtett" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nascosto" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "מוסתר" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "非表示" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "숨김" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Verborgen" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Skjult" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ukryte" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Oculto" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Oculto" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ascuns" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Скрыто" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Скривено" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dold" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Gizli" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Приховано" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Đã ẩn" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "隐藏" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "隱藏" - } - } - } - }, - "Move to button row" : { - "localizations" : { - "af" : { - "stringUnit" : { - "state" : "translated", - "value" : "Skuif na knoppiery" - } - }, - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "نقل إلى صف الأزرار" - } - }, - "bg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Преместване в реда с бутони" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mou a la fila de botons" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Přesunout do řádku tlačítek" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Flyt til knaprække" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "In die Schaltflächenzeile verschieben" - } - }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "Μετακίνηση στη σειρά κουμπιών" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Move to button row" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mover a la fila de botones" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Siirrä painikeriville" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Déplacer vers la rangée de boutons" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Áthelyezés a gombsorba" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sposta nella riga pulsanti" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "העבר לשורת הכפתורים" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "ボタン行へ移動" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "버튼 행으로 이동" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Naar knoppenrij verplaatsen" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Flytt til knapperad" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Przenieś do wiersza przycisków" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mover para a linha de botões" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mover para a linha de botões" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mută în rândul de butoane" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Переместить в строку кнопок" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Премести у ред дугмади" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Flytta till knapprad" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Düğme satırına taşı" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Перемістити до рядка кнопок" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Chuyển vào hàng nút" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "移动到按钮行" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "移至按鈕列" - } - } - } - }, - "Move to More menu" : { - "localizations" : { - "af" : { - "stringUnit" : { - "state" : "translated", - "value" : "Skuif na Meer-kieslys" - } - }, - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "نقل إلى قائمة المزيد" - } - }, - "bg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Преместване в менюто „Още“" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mou al menú Més" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Přesunout do nabídky Další" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Flyt til menuen Mere" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ins Mehr-Menü verschieben" - } - }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "Μετακίνηση στο μενού Περισσότερα" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Move to More menu" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mover al menú Más" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Siirrä Lisää-valikkoon" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Déplacer vers le menu Plus" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Áthelyezés a Továbbiak menübe" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sposta nel menu Altro" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "העבר לתפריט עוד" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "その他メニューへ移動" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "더 보기 메뉴로 이동" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Naar Meer-menu verplaatsen" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Flytt til Mer-meny" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Przenieś do menu Więcej" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mover para o menu Mais" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mover para o menu Mais" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mută în meniul Mai multe" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Переместить в меню Ещё" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Премести у мени Још" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Flytta till menyn Mer" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Daha Fazla menüsüne taşı" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Перемістити до меню Ще" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Chuyển vào menu Thêm" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "移动到更多菜单" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "移至更多選單" - } - } - } - }, - "Hide action" : { - "localizations" : { - "af" : { - "stringUnit" : { - "state" : "translated", - "value" : "Versteek aksie" - } - }, - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "إخفاء الإجراء" - } - }, - "bg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Скриване на действието" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Amaga l’acció" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Skrýt akci" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Skjul handling" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aktion ausblenden" - } - }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "Απόκρυψη ενέργειας" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Hide action" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ocultar acción" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Piilota toiminto" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Masquer l’action" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Művelet elrejtése" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nascondi azione" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "הסתר פעולה" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "アクションを非表示" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "동작 숨기기" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Actie verbergen" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Skjul handling" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ukryj akcję" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ocultar ação" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ocultar ação" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ascunde acțiunea" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Скрыть действие" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Сакриј радњу" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dölj åtgärd" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Eylemi gizle" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Приховати дію" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ẩn hành động" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "隐藏操作" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "隱藏操作" - } - } - } - }, - "Reply" : { - "localizations" : { - "af" : { - "stringUnit" : { - "state" : "translated", - "value" : "Antwoord" - } - }, - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "رد" - } - }, - "bg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Отговор" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Respondre" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Odpovědět" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Svar" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Antworten" - } - }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "Απάντηση" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Reply" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Responder" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vastaa" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Répondre" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Válasz" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Rispondi" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "תגובה" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "返信" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "답글" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Beantwoorden" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Svar" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Odpowiedz" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Responder" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Responder" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Răspunde" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ответить" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Odgovori" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Svara" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Yanıtla" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Відповісти" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Trả lời" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "回复" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "回覆" - } - } - } - }, - "Comment" : { - "localizations" : { - "af" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kommentaar" - } - }, - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "تعليق" - } - }, - "bg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Коментар" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Comenta" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Komentovat" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kommentar" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kommentieren" - } - }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "Σχόλιο" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Comment" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Comentario" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kommentti" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Commenter" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Hozzászólás" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Commenta" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "תגובה" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "コメント" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "댓글" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Reactie" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kommentar" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Komentarz" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Comentar" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Comentar" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Comentariu" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Комментировать" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Коментариши" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kommentera" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Yorum yap" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Коментувати" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bình luận" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "评论" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "留言" - } - } - } - }, - "Repost" : { - "localizations" : { - "af" : { - "stringUnit" : { - "state" : "translated", - "value" : "Herplaas" - } - }, - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "إعادة نشر" - } - }, - "bg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Препубликуване" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Republicar" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Přesdílet" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Post igen" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Teilen" - } - }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "Αναδημοσίευση" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Repost" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Compartir" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Jaa" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Repartager" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Újraközlés" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ripubblica" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "פרסום מחדש" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "リポスト" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "리포스트" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Delen" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Reposter" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Podaj dalej" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Repostar" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Republicar" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Repostare" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Репост" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ponovo objavi" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Reposta" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Yeniden paylaş" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Поширити" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Đăng lại" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "转发" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "轉發" - } - } - } - }, - "Quote" : { - "localizations" : { - "af" : { - "stringUnit" : { - "state" : "translated", - "value" : "Haal aan" - } - }, - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "اقتباس" - } - }, - "bg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Цитат" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Citar" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Citovat" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Citér" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zitieren" - } - }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "Παράθεση" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Quote" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Citar" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lainaa" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Citer" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Idézés" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cita" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "ציטוט" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "引用" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "인용" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Citeren" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Siter" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cytat" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Citar" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Citar" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Citat" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Цитировать" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Citiraj" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Citera" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Alıntıla" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Цитата" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Trích dẫn" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "引用" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "引用" - } - } - } - }, - "Like" : { - "localizations" : { - "af" : { - "stringUnit" : { - "state" : "translated", - "value" : "Hou van" - } - }, - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "إعجاب" - } - }, - "bg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Харесване" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "M'agrada" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "To se mi líbí" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Synes godt om" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Gefällt mir" - } - }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "Μου αρέσει" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Like" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Me gusta" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tykkää" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "J'aime" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kedvelés" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mi piace" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "אהבתי" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "いいね" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "좋아요" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vind ik leuk" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lik" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lubię to!" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Curtir" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Gostar" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Apreciază" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Лайк" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sviđa mi se" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Gilla" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Beğen" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Подобається" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Thích" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "赞" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "喜歡" - } - } - } - }, - "React" : { - "localizations" : { - "af" : { - "stringUnit" : { - "state" : "translated", - "value" : "Voeg reaksie by" - } - }, - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "إضافة تفاعل" - } - }, - "bg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Добави реакция" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Afegir reacció" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Přidat reakci" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tilføj reaktion" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Reaktion hinzufügen" - } - }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "Προσθήκη αντίδρασης" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "React" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Añadir reacción" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lisää reaktio" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ajouter une réaction" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Reakció hozzáadása" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aggiungi reazione" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "הוסף תגובה" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "リアクション" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "반응" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Reactie toevoegen" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Legg til reaksjon" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dodaj reakcję" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Adicionar reação" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Adicionar reação" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Adaugă reacție" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Добавить реакцию" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dodaj reakciju" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lägg till reaktion" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tepki ekle" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Додати реакцію" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Thêm cảm xúc" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "回应" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "反應" - } - } - } - }, - "Bookmark" : { - "localizations" : { - "af" : { - "stringUnit" : { - "state" : "translated", - "value" : "Voeg boekmerk by" - } - }, - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "إضافة إشارة مرجعية" - } - }, - "bg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Добави отметка" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Afegir marcador" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Přidat do záložek" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tilføj bogmærke" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lesezeichen hinzufügen" - } - }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "Προσθήκη σελιδοδείκτη" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bookmark" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Añadir marcador" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lisää kirjanmerkki" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ajouter un signet" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Könyvjelző hozzáadása" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aggiungi segnalibro" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "הוסף סימנייה" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "ブックマーク" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "북마크" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bladwijzer toevoegen" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Legg til bokmerke" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dodaj zakładkę" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Salvar favorito" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Adicionar marcador" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Adaugă semn de carte" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Добавить в закладки" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dodaj obeleživač" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lägg till bokmärke" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Yer işareti ekle" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "У закладки" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Thêm dấu trang" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "书签" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "書籤" - } - } - } - }, - "Favorite" : { - "localizations" : { - "af" : { - "stringUnit" : { - "state" : "translated", - "value" : "Gunsteling" - } - }, - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "تفضيل" - } - }, - "bg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Любими" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Preferit" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Oblíbené" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Favorit" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Favorisieren" - } - }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "Αγαπημένο" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Favorite" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Favorito" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Suosikki" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Favori" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kedvenc" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Preferito" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "מועדף" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "お気に入り" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "즐겨찾기" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Favoriet" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Favoritt" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ulubione" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Favoritar" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Favorito" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Favorit" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "В избранное" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Omiljeno" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Favoritmarkera" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Favorilere ekle" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "У вибране" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Yêu thích" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "收藏" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "收藏" - } - } - } - }, - "Share" : { - "localizations" : { - "af" : { - "stringUnit" : { - "state" : "translated", - "value" : "Deel" - } - }, - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "مشاركة" - } - }, - "bg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Споделяне" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Compartir" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sdílet" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Del" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Teilen" - } - }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "Κοινοποίηση" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Share" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Compartir" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Jaa" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Partager" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Megosztás" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Condividi" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "שיתוף" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "共有" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "공유" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Delen" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Del" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Udostępnij" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Compartilhar" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Partilhar" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Partajează" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Поделиться" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Podeli" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dela" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Paylaş" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Поділитися" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Chia sẻ" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "分享" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "分享" - } - } - } - }, - "Fx share" : { - "localizations" : { - "af" : { - "stringUnit" : { - "state" : "translated", - "value" : "Deel via FxEmbed" - } - }, - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "مشاركة عبر FxEmbed" - } - }, - "bg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Споделяне чрез FxEmbed" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Compartir via FxEmbed" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sdílet přes FxEmbed" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Del via FxEmbed" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Über FxEmbed teilen" - } - }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "Κοινοποίηση μέσω FxEmbed" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Fx share" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Compartir vía FxEmbed" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Jaa FxEmbedin kautta" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Partager via FxEmbed" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Megosztás FxEmbed-en keresztül" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Condividi tramite FxEmbed" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "שתף דרך FxEmbed" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "Fx共有" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "Fx 공유" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Delen via FxEmbed" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Del via FxEmbed" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Udostępnij przez FxEmbed" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Compartilhar via FxEmbed" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Partilhar via FxEmbed" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Partajează prin FxEmbed" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Поделиться через FxEmbed" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Podeli putem FxEmbed" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dela via FxEmbed" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "FxEmbed ile paylaş" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Поділитися через FxEmbed" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Chia sẻ qua FxEmbed" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "Fx 分享" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "Fx 分享" - } - } - } - }, - "Report" : { - "localizations" : { - "af" : { - "stringUnit" : { - "state" : "translated", - "value" : "Rapporteer" - } - }, - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "إبلاغ" - } - }, - "bg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Докладване" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Informar" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nahlásit" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Anmeld" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Melden" - } - }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "Αναφορά" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Report" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Reportar" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Raportoi" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Signaler" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Jelentés" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Segnala" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "דיווח" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "報告" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "신고" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Rapporteren" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Rapporter" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zgłoś" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Denunciar" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Denunciar" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Raportează" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Пожаловаться" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Prijavi" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Rapportera" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bildir" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Скаржитися" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Báo cáo" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "举报" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "檢舉" - } - } - } - }, - "Mute user" : { - "localizations" : { - "af" : { - "stringUnit" : { - "state" : "translated", - "value" : "Demp" - } - }, - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "كتم" - } - }, - "bg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Заглушаване" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Silenciar" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Skrýt" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lydløs" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Stummschalten" - } - }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "Σίγαση" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mute user" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Silenciar" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mykistä" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Masquer" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Némítás" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Silenzia" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "השתקה" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "ユーザーをミュート" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "사용자 뮤트" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dempen" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Demp" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Wycisz" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Silenciar" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Silenciar" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ignoră" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Игнорировать" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Utišaj" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tysta" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sessize al" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Приглушити" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ẩn" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "静音用户" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "靜音使用者" - } - } - } - }, - "Block user" : { - "localizations" : { - "af" : { - "stringUnit" : { - "state" : "translated", - "value" : "Blokkeer" - } - }, - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "حظر" - } - }, - "bg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Блокиране" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bloquejar" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Blokovat" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bloker" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Blockieren" - } - }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "Αποκλεισμός" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Block user" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bloquear" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Estä" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bloquer" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tiltás" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Blocca" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "חסימה" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "ユーザーをブロック" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "사용자 차단" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Blokkeren" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Blokker" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zablokuj" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bloquear" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bloquear" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Blochează" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Заблокировать" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Blokiraj" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Blockera" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Engelle" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Заблокувати" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Chặn" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "屏蔽用户" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "封鎖使用者" - } - } - } } }, "version" : "1.1" diff --git a/iosApp/flare/SplashScreen.storyboard b/appleApp/ios/SplashScreen.storyboard similarity index 100% rename from iosApp/flare/SplashScreen.storyboard rename to appleApp/ios/SplashScreen.storyboard diff --git a/appleApp/ios/UI/AgentTrace+LocalizedLabel.swift b/appleApp/ios/UI/AgentTrace+LocalizedLabel.swift new file mode 100644 index 0000000000..476a9a07c3 --- /dev/null +++ b/appleApp/ios/UI/AgentTrace+LocalizedLabel.swift @@ -0,0 +1,89 @@ +import Foundation +import FlareAppleCore +import KotlinSharedUI + +extension AgentTrace { + var localizedLabel: String { + if let toolKey { + return toolKey.localizedLabel + } + + return switch phase { + case .loadingPostContext: + localizedPresentationString("status_insight_trace_loading_post_context", fallback: "Loading post context") + case .postContextLoaded: + localizedPresentationString("status_insight_trace_post_context_loaded", fallback: "Post context loaded") + case .preparingImages: + localizedPresentationString("status_insight_trace_preparing_images", fallback: "Preparing images") + case .imagesUnsupportedFallback: + localizedPresentationString("status_insight_trace_images_unsupported_fallback", fallback: "Images are not supported, using text fallback") + case .agentStarted: + localizedPresentationString("status_insight_trace_agent_started", fallback: "Agent started") + case .strategyStarted: + localizedPresentationString("status_insight_trace_strategy_started", fallback: "Strategy started") + case .strategyCompleted: + localizedPresentationString("status_insight_trace_strategy_completed", fallback: "Strategy completed") + case .subgraphStarted: + localizedPresentationString("status_insight_trace_subgraph_started", fallback: "Subgraph started") + case .subgraphCompleted: + localizedPresentationString("status_insight_trace_subgraph_completed", fallback: "Subgraph completed") + case .subgraphFailed: + localizedPresentationString("status_insight_trace_subgraph_failed", fallback: "Subgraph failed") + case .askingModel: + localizedPresentationString("status_insight_trace_asking_model", fallback: "Asking model %@", arguments: [detail ?? ""]) + case .modelResponseReceived: + localizedPresentationString("status_insight_trace_model_response_received", fallback: "Model response received") + case .streamingStarted: + localizedPresentationString("status_insight_trace_streaming_started", fallback: "Streaming started %@", arguments: [detail ?? ""]) + case .streamingResponse: + localizedPresentationString("status_insight_trace_streaming_response", fallback: "Streaming response") + case .streamingCompleted: + localizedPresentationString("status_insight_trace_streaming_completed", fallback: "Streaming completed") + case .streamingFailed: + localizedPresentationString("status_insight_trace_streaming_failed", fallback: "Streaming failed") + case .runningStep: + localizedPresentationString("status_insight_trace_running_step", fallback: "Running step") + case .stepCompleted: + localizedPresentationString("status_insight_trace_step_completed", fallback: "Step completed") + case .stepFailed: + localizedPresentationString("status_insight_trace_step_failed", fallback: "Step failed") + case .toolCallStarted: + detail ?? localizedPresentationString("status_insight_trace_running_step", fallback: "Running step") + case .toolCallCompleted: + detail ?? localizedPresentationString("status_insight_trace_step_completed", fallback: "Step completed") + case .toolValidationFailed: + detail ?? localizedPresentationString("status_insight_trace_tool_validation_failed", fallback: "Tool validation failed") + case .toolCallFailed: + detail ?? localizedPresentationString("status_insight_trace_tool_call_failed", fallback: "Tool call failed") + case .agentCompleted: + localizedPresentationString("status_insight_trace_agent_completed", fallback: "Agent completed") + case .agentFailed: + localizedPresentationString("status_insight_trace_agent_failed", fallback: "Agent failed") + case .agentClosing: + localizedPresentationString("status_insight_trace_agent_closing", fallback: "Agent closing") + } + } +} + +private extension AgentToolKey { + var localizedLabel: String { + return switch self { + case .loadStatusContextStarted: + localizedPresentationString("status_insight_trace_tool_load_status_context_started", fallback: "Loading status context") + case .loadStatusContextCompleted: + localizedPresentationString("status_insight_trace_tool_load_status_context_completed", fallback: "Loaded status context") + case .loadStatusContextValidationFailed: + localizedPresentationString("status_insight_trace_tool_load_status_context_validation_failed", fallback: "Status context validation failed") + case .loadStatusContextFailed: + localizedPresentationString("status_insight_trace_tool_load_status_context_failed", fallback: "Failed to load status context") + case .searchPostsStarted, .searchUsersStarted: + localizedPresentationString("status_insight_trace_tool_search_status_started", fallback: "Searching statuses") + case .searchPostsCompleted, .searchUsersCompleted: + localizedPresentationString("status_insight_trace_tool_search_status_completed", fallback: "Search completed") + case .searchPostsValidationFailed, .searchUsersValidationFailed: + localizedPresentationString("status_insight_trace_tool_search_status_validation_failed", fallback: "Search validation failed") + case .searchPostsFailed, .searchUsersFailed: + localizedPresentationString("status_insight_trace_tool_search_status_failed", fallback: "Search failed") + } + } +} diff --git a/iosApp/flare/UI/Component/AdaptiveTimelineCardUIView.swift b/appleApp/ios/UI/Component/AdaptiveTimelineCardUIView.swift similarity index 100% rename from iosApp/flare/UI/Component/AdaptiveTimelineCardUIView.swift rename to appleApp/ios/UI/Component/AdaptiveTimelineCardUIView.swift diff --git a/iosApp/flare/UI/Component/AgentChatView.swift b/appleApp/ios/UI/Component/AgentChatView.swift similarity index 99% rename from iosApp/flare/UI/Component/AgentChatView.swift rename to appleApp/ios/UI/Component/AgentChatView.swift index 8b80a1ec78..9b7a5e832d 100644 --- a/iosApp/flare/UI/Component/AgentChatView.swift +++ b/appleApp/ios/UI/Component/AgentChatView.swift @@ -2,6 +2,7 @@ import ChatLayout import SwiftUI import SwiftUIBackports import KotlinSharedUI +import FlareAppleUI struct AgentChatView: View { let messages: [AgentChatHistoryMessage] diff --git a/iosApp/flare/UI/Component/Backport.swift b/appleApp/ios/UI/Component/Backport.swift similarity index 75% rename from iosApp/flare/UI/Component/Backport.swift rename to appleApp/ios/UI/Component/Backport.swift index 4da1e8c76a..2d0c326c8d 100644 --- a/iosApp/flare/UI/Component/Backport.swift +++ b/appleApp/ios/UI/Component/Backport.swift @@ -1,32 +1,44 @@ import SwiftUI import SwiftUIBackports -extension Backport where Content: View { +public extension Backport where Content: View { @ViewBuilder func labelIconToTitleSpacing(_ spacing: CGFloat) -> some View { + #if os(iOS) if #available(iOS 26.0, *) { content.labelIconToTitleSpacing(spacing) } else { content.labelStyle(BackportLabelStyle(spacing: spacing)) } + #else + content.labelStyle(BackportLabelStyle(spacing: spacing)) + #endif } @ViewBuilder func navigationSubtitle(_ subtitle: Text) -> some View { + #if os(iOS) if #available(iOS 26.0, *) { content.navigationSubtitle(subtitle) } else { content } + #else + content + #endif } @ViewBuilder func navigationSubtitle(_ subtitle: S) -> some View where S : StringProtocol { + #if os(iOS) if #available(iOS 26.0, *) { content.navigationSubtitle(subtitle) } else { content } + #else + content + #endif } } @@ -40,10 +52,10 @@ struct BackportLabelStyle: LabelStyle { } } -extension Backport where Content: View { +public extension Backport where Content: View { @ViewBuilder func textRenderer(_ renderer: T) -> some View where T : TextRenderer { - if #available(iOS 18.0, *) { + if #available(iOS 18.0, macOS 15.0, *) { content.textRenderer(renderer) } else { content diff --git a/iosApp/flare/UI/Component/BackportWebView.swift b/appleApp/ios/UI/Component/BackportWebView.swift similarity index 100% rename from iosApp/flare/UI/Component/BackportWebView.swift rename to appleApp/ios/UI/Component/BackportWebView.swift diff --git a/iosApp/flare/UI/Component/CollectionViewTimeline.swift b/appleApp/ios/UI/Component/CollectionViewTimeline.swift similarity index 99% rename from iosApp/flare/UI/Component/CollectionViewTimeline.swift rename to appleApp/ios/UI/Component/CollectionViewTimeline.swift index d56787516b..2c82136549 100644 --- a/iosApp/flare/UI/Component/CollectionViewTimeline.swift +++ b/appleApp/ios/UI/Component/CollectionViewTimeline.swift @@ -1,4 +1,5 @@ import SwiftUI +import FlareAppleUI import KotlinSharedUI import CHTCollectionViewWaterfallLayout import GSPlayer diff --git a/iosApp/flare/UI/Component/DMConversationMessagesView.swift b/appleApp/ios/UI/Component/DMConversationMessagesView.swift similarity index 99% rename from iosApp/flare/UI/Component/DMConversationMessagesView.swift rename to appleApp/ios/UI/Component/DMConversationMessagesView.swift index 11fd65b976..05c554a998 100644 --- a/iosApp/flare/UI/Component/DMConversationMessagesView.swift +++ b/appleApp/ios/UI/Component/DMConversationMessagesView.swift @@ -1,6 +1,8 @@ import ChatLayout import Kingfisher import SwiftUI +import FlareAppleUI +import AppleFontAwesome @preconcurrency import KotlinSharedUI struct DMConversationMessagesView: UIViewControllerRepresentable { @@ -591,7 +593,7 @@ private final class DMMessageUIView: UIView, TimelineHeightProviding { avatarView.isHidden = true retryButton.isHidden = true retryButton.tintColor = .systemRed - retryButton.setImage(UIImage(named: "fa-circle-exclamation"), for: .normal) + retryButton.setImage(UIImage(fontAwesome: .circleExclamation), for: .normal) retryButton.addTarget(self, action: #selector(retryTapped), for: .touchUpInside) senderNameView.lineLimit = 1 @@ -1027,7 +1029,7 @@ private final class DMBubbleView: UIView { private final class DMMediaPreviewView: UIView { private let imageView = UIImageView() - private let playIconView = UIImageView(image: UIImage(named: "fa-circle-play")) + private let playIconView = UIImageView(image: UIImage(fontAwesome: .circlePlay)) private var aspectRatio: CGFloat = 1 private var onOpenURL: ((URL) -> Void)? private var imageRouteURL: URL? diff --git a/iosApp/flare/UI/Component/EmojiPopup.swift b/appleApp/ios/UI/Component/EmojiPopup.swift similarity index 94% rename from iosApp/flare/UI/Component/EmojiPopup.swift rename to appleApp/ios/UI/Component/EmojiPopup.swift index a602572726..3ddf64f52a 100644 --- a/iosApp/flare/UI/Component/EmojiPopup.swift +++ b/appleApp/ios/UI/Component/EmojiPopup.swift @@ -1,14 +1,16 @@ import SwiftUI import KotlinSharedUI +import FlareAppleCore +import FlareAppleUI import SwiftUIBackports -struct EmojiPopup: View { +public struct EmojiPopup: View { @StateObject private var presenter: KotlinPresenter @State private var filterText: String = "" - let data: EmojiData - let onItemClicked: (UiEmoji) -> Void + private let data: EmojiData + private let onItemClicked: (UiEmoji) -> Void - var body: some View { + public var body: some View { List { StateView(state: presenter.state.history) { history in let items = history.cast(UiEmoji.self) @@ -100,7 +102,7 @@ struct EmojiSection: View { } } } -extension EmojiPopup { +public extension EmojiPopup { init( data: EmojiData, onItemClicked: @escaping (UiEmoji) -> Void diff --git a/iosApp/flare/UI/Component/GalleryTimelinePagingView.swift b/appleApp/ios/UI/Component/GalleryTimelinePagingView.swift similarity index 99% rename from iosApp/flare/UI/Component/GalleryTimelinePagingView.swift rename to appleApp/ios/UI/Component/GalleryTimelinePagingView.swift index 3c4c895b9a..96d6feb385 100644 --- a/iosApp/flare/UI/Component/GalleryTimelinePagingView.swift +++ b/appleApp/ios/UI/Component/GalleryTimelinePagingView.swift @@ -1,8 +1,11 @@ import SwiftUI +import FlareAppleUI import UIKit import Kingfisher import KotlinSharedUI import CHTCollectionViewWaterfallLayout +import FlareAppleCore +import AppleFontAwesome // MARK: - SwiftUI Wrapper @@ -953,7 +956,7 @@ private final class GalleryPostTileUIView: UIView, UIGestureRecognizerDelegate { private let stack = UIStackView() private let imageView = GalleryNetworkImageView(frame: .zero) private let playBadge: UIImageView = { - let imageView = UIImageView(image: UIImage(named: "fa-circle-play")) + let imageView = UIImageView(image: UIImage(fontAwesome: .circlePlay)) imageView.contentMode = .scaleAspectFit imageView.tintColor = .white imageView.translatesAutoresizingMaskIntoConstraints = false diff --git a/iosApp/flare/UI/Component/IsScrolling.swift b/appleApp/ios/UI/Component/IsScrolling.swift similarity index 84% rename from iosApp/flare/UI/Component/IsScrolling.swift rename to appleApp/ios/UI/Component/IsScrolling.swift index 7117345659..e9e1ca8fa6 100644 --- a/iosApp/flare/UI/Component/IsScrolling.swift +++ b/appleApp/ios/UI/Component/IsScrolling.swift @@ -5,15 +5,19 @@ private struct IsScrollingKey: EnvironmentKey { } @Observable -final class IsScrollingState { - var isScrolling = false +public final class IsScrollingState { + public var isScrolling: Bool + + public init(isScrolling: Bool = false) { + self.isScrolling = isScrolling + } } private struct IsScrollingStateKey: EnvironmentKey { static let defaultValue: IsScrollingState? = nil } -extension EnvironmentValues { +public extension EnvironmentValues { var isScrolling: Bool { get { self[IsScrollingKey.self] } set { self[IsScrollingKey.self] = newValue } @@ -34,13 +38,13 @@ private struct DetectScrollingModifier: ViewModifier { @State private var debounceTask: Task? = nil func body(content: Content) -> some View { - if #available(iOS 18.0, *) { + if #available(iOS 18.0, macOS 15.0, *) { content .environment(\.isScrolling, isScrolling) - .onScrollPhaseChange { old, phase in + .onScrollPhaseChange { _, phase in rawIsScrolling = (phase != .idle) } - .onChange(of: rawIsScrolling) { old, newValue in + .onChange(of: rawIsScrolling) { _, newValue in if newValue { debounceTask?.cancel() isScrolling = true @@ -66,7 +70,7 @@ private struct DetectScrollingModifier: ViewModifier { } } -extension View { +public extension View { @ViewBuilder func detectScrolling(debounceIdle: TimeInterval = 0.500) -> some View { if #available(iOS 17.0, macOS 14.0, *) { diff --git a/iosApp/flare/UI/Component/ListEmptyUIView.swift b/appleApp/ios/UI/Component/ListEmptyUIView.swift similarity index 100% rename from iosApp/flare/UI/Component/ListEmptyUIView.swift rename to appleApp/ios/UI/Component/ListEmptyUIView.swift diff --git a/iosApp/flare/UI/Component/ListErrorUIView.swift b/appleApp/ios/UI/Component/ListErrorUIView.swift similarity index 100% rename from iosApp/flare/UI/Component/ListErrorUIView.swift rename to appleApp/ios/UI/Component/ListErrorUIView.swift diff --git a/iosApp/flare/UI/Component/MergePolicySettingsItem.swift b/appleApp/ios/UI/Component/MergePolicySettingsItem.swift similarity index 67% rename from iosApp/flare/UI/Component/MergePolicySettingsItem.swift rename to appleApp/ios/UI/Component/MergePolicySettingsItem.swift index fcc87a3e98..30d4a62bdd 100644 --- a/iosApp/flare/UI/Component/MergePolicySettingsItem.swift +++ b/appleApp/ios/UI/Component/MergePolicySettingsItem.swift @@ -1,10 +1,14 @@ import SwiftUI import KotlinSharedUI -struct MergePolicySettingsItem: View { - @Binding var selected: TimelineMergePolicy +public struct MergePolicySettingsItem: View { + @Binding private var selected: TimelineMergePolicy - var body: some View { + public init(selected: Binding) { + self._selected = selected + } + + public var body: some View { Picker(selection: $selected) { Text("tab_settings_merge_policy_time").tag(TimelineMergePolicy.time) Text("tab_settings_merge_policy_time_per_page").tag(TimelineMergePolicy.timePerPage) diff --git a/appleApp/ios/UI/Component/PagingView.swift b/appleApp/ios/UI/Component/PagingView.swift new file mode 100644 index 0000000000..0d021c920a --- /dev/null +++ b/appleApp/ios/UI/Component/PagingView.swift @@ -0,0 +1,20 @@ +import SwiftUI +import KotlinSharedUI +import FlareAppleCore +import FlareAppleUI + +struct UserPagingView: View { + @Environment(\.openURL) private var openURL + let data: PagingState + var body: some View { + PagingView(data: data) { user in + UserCompatView(data: user) + .onTapGesture { + user.onClicked(ClickContext(launcher: AppleUriLauncher(openUrl: openURL))) + } + } loadingContent: { + UserLoadingView() + .padding(.vertical, 8) + } + } +} diff --git a/iosApp/flare/UI/Component/SafariView.swift b/appleApp/ios/UI/Component/SafariView.swift similarity index 100% rename from iosApp/flare/UI/Component/SafariView.swift rename to appleApp/ios/UI/Component/SafariView.swift diff --git a/iosApp/flare/UI/Component/SearchHistorySuggestions.swift b/appleApp/ios/UI/Component/SearchHistorySuggestions.swift similarity index 70% rename from iosApp/flare/UI/Component/SearchHistorySuggestions.swift rename to appleApp/ios/UI/Component/SearchHistorySuggestions.swift index 4d50f7d824..9f87eb7a82 100644 --- a/iosApp/flare/UI/Component/SearchHistorySuggestions.swift +++ b/appleApp/ios/UI/Component/SearchHistorySuggestions.swift @@ -1,13 +1,25 @@ import SwiftUI @preconcurrency import KotlinSharedUI -struct SearchHistorySuggestions: View { - let state: SearchHistoryState - let searchText: String - let onSelect: (String) -> Void - let onDelete: (String) -> Void +public struct SearchHistorySuggestions: View { + private let state: SearchHistoryState + private let searchText: String + private let onSelect: (String) -> Void + private let onDelete: (String) -> Void - var body: some View { + public init( + state: SearchHistoryState, + searchText: String, + onSelect: @escaping (String) -> Void, + onDelete: @escaping (String) -> Void + ) { + self.state = state + self.searchText = searchText + self.onSelect = onSelect + self.onDelete = onDelete + } + + public var body: some View { if case .success(let data) = onEnum(of: state.searchHistories) { ForEach(filteredHistories(data.data), id: \.keyword) { item in Button { diff --git a/iosApp/flare/UI/Component/Status/FeedUIView.swift b/appleApp/ios/UI/Component/Status/FeedUIView.swift similarity index 99% rename from iosApp/flare/UI/Component/Status/FeedUIView.swift rename to appleApp/ios/UI/Component/Status/FeedUIView.swift index b662ad9a1d..c054631090 100644 --- a/iosApp/flare/UI/Component/Status/FeedUIView.swift +++ b/appleApp/ios/UI/Component/Status/FeedUIView.swift @@ -2,6 +2,7 @@ import UIKit import Kingfisher import KotlinSharedUI import SwiftUI +import FlareAppleCore /// UIKit port of `FeedView`. /// diff --git a/iosApp/flare/UI/Component/Status/RichTextUIView.swift b/appleApp/ios/UI/Component/Status/RichTextUIView.swift similarity index 99% rename from iosApp/flare/UI/Component/Status/RichTextUIView.swift rename to appleApp/ios/UI/Component/Status/RichTextUIView.swift index d362eb3522..92c8b996d6 100644 --- a/iosApp/flare/UI/Component/Status/RichTextUIView.swift +++ b/appleApp/ios/UI/Component/Status/RichTextUIView.swift @@ -2,6 +2,7 @@ import UIKit import Kingfisher import KotlinSharedUI import SwiftUI +import FlareAppleCore /// Pure-UIKit renderer for the iOS `RichText` SwiftUI view. final class RichTextUIView: UIView, TimelineHeightProviding { diff --git a/iosApp/flare/UI/Component/Status/StatusShareSheet.swift b/appleApp/ios/UI/Component/Status/StatusShareSheet.swift similarity index 98% rename from iosApp/flare/UI/Component/Status/StatusShareSheet.swift rename to appleApp/ios/UI/Component/Status/StatusShareSheet.swift index 4687b07d35..6a6d25f0ce 100644 --- a/iosApp/flare/UI/Component/Status/StatusShareSheet.swift +++ b/appleApp/ios/UI/Component/Status/StatusShareSheet.swift @@ -2,6 +2,9 @@ import SwiftUI import KotlinSharedUI import SwiftUIBackports import UniformTypeIdentifiers +import FlareAppleCore +import FlareAppleUI +import AppleFontAwesome struct StatusShareSheet: View { let statusKey: MicroBlogKey @@ -98,7 +101,7 @@ struct StatusShareSheet: View { Label { Text("Cancel") } icon: { - Image("fa-xmark") + Image(fontAwesome: .xmark) } } } diff --git a/iosApp/flare/UI/Component/Status/StatusTopMessageUIView.swift b/appleApp/ios/UI/Component/Status/StatusTopMessageUIView.swift similarity index 97% rename from iosApp/flare/UI/Component/Status/StatusTopMessageUIView.swift rename to appleApp/ios/UI/Component/Status/StatusTopMessageUIView.swift index b42679c2a3..29ea1ec752 100644 --- a/iosApp/flare/UI/Component/Status/StatusTopMessageUIView.swift +++ b/appleApp/ios/UI/Component/Status/StatusTopMessageUIView.swift @@ -1,5 +1,8 @@ import UIKit import KotlinSharedUI +import FlareAppleCore +import FlareAppleUI +import AppleFontAwesome /// UIKit port of StatusTopMessageView: icon + user name + localized text. final class StatusTopMessageUIView: UIView, ManualLayoutMeasurable, TimelineHeightProviding { @@ -51,7 +54,7 @@ final class StatusTopMessageUIView: UIView, ManualLayoutMeasurable, TimelineHeig self.topMessage = message self.topMessageOnly = topMessageOnly - iconView.image = UIImage(named: message.icon.imageName) + iconView.image = UIImage(fontAwesome: message.icon.fontAwesomeIcon) if let user = message.user { nameView.isHidden = false @@ -230,4 +233,4 @@ final class StatusTopMessageUIView: UIView, ManualLayoutMeasurable, TimelineHeig } import SwiftUI // for OpenURLAction -// `UiIcon.imageName` is declared in StatusActionView.swift. +// `UiIcon.fontAwesomeIcon` is declared in the iOS target presentation mappings. diff --git a/iosApp/flare/UI/Component/Status/StatusTranslateUIView.swift b/appleApp/ios/UI/Component/Status/StatusTranslateUIView.swift similarity index 99% rename from iosApp/flare/UI/Component/Status/StatusTranslateUIView.swift rename to appleApp/ios/UI/Component/Status/StatusTranslateUIView.swift index d2a4465515..99b27e1191 100644 --- a/iosApp/flare/UI/Component/Status/StatusTranslateUIView.swift +++ b/appleApp/ios/UI/Component/Status/StatusTranslateUIView.swift @@ -1,6 +1,7 @@ import UIKit import Combine import KotlinSharedUI +import FlareAppleCore /// UIKit translation / summary renderer for status detail cells. /// diff --git a/iosApp/flare/UI/Component/Status/StatusUIKitActions.swift b/appleApp/ios/UI/Component/Status/StatusUIKitActions.swift similarity index 98% rename from iosApp/flare/UI/Component/Status/StatusUIKitActions.swift rename to appleApp/ios/UI/Component/Status/StatusUIKitActions.swift index 3e9adad719..764ea29ec1 100644 --- a/iosApp/flare/UI/Component/Status/StatusUIKitActions.swift +++ b/appleApp/ios/UI/Component/Status/StatusUIKitActions.swift @@ -1,5 +1,7 @@ import UIKit import KotlinSharedUI +import FlareAppleCore +import AppleFontAwesome // MARK: - StatusActionsUIView // Mirrors StatusActionsView/StatusActionView/StatusActionItemView in @@ -284,7 +286,7 @@ final class StatusActionsUIView: UIView, ManualLayoutMeasurable, TimelineHeightP private func makeItemButton(item: ActionMenu.Item, isFixedWidth: Bool) -> UIView { let title: String? if useText, let text = item.text?.resolvedString { - title = String(localized: text) + title = text } else if showNumbers, let count = item.count?.humanized { title = count } else { @@ -296,7 +298,7 @@ final class StatusActionsUIView: UIView, ManualLayoutMeasurable, TimelineHeightP let control = itemButtonPool[itemButtonCursor] itemButtonCursor += 1 control.configure( - image: item.icon.flatMap { UIImage(named: $0.imageName) }, + image: item.icon.flatMap { UIImage(fontAwesome: $0.fontAwesomeIcon) }, title: title, tintColor: item.color?.uiColor ?? .secondaryLabel, font: actionFont, @@ -325,7 +327,7 @@ final class StatusActionsUIView: UIView, ManualLayoutMeasurable, TimelineHeightP let control = groupButtonPool[groupButtonCursor] groupButtonCursor += 1 control.configure( - image: group.displayItem.icon.flatMap { UIImage(named: $0.imageName) }, + image: group.displayItem.icon.flatMap { UIImage(fontAwesome: $0.fontAwesomeIcon) }, title: title, tintColor: group.displayItem.color?.uiColor ?? .secondaryLabel, font: actionFont, @@ -378,11 +380,11 @@ final class StatusActionsUIView: UIView, ManualLayoutMeasurable, TimelineHeightP case .item(let item): let title: String if let text = item.text?.resolvedString { - title = String(localized: text) + title = text } else { title = "" } - let image = item.icon.flatMap { UIImage(named: $0.imageName) } + let image = item.icon.flatMap { UIImage(fontAwesome: $0.fontAwesomeIcon) } let uiAction = UIAction( title: title, image: image, @@ -397,7 +399,7 @@ final class StatusActionsUIView: UIView, ManualLayoutMeasurable, TimelineHeightP case .group(let g): let subtitle: String if let text = g.displayItem.text?.resolvedString { - subtitle = String(localized: text) + subtitle = text } else { subtitle = "" } diff --git a/iosApp/flare/UI/Component/Status/StatusUIKitCard.swift b/appleApp/ios/UI/Component/Status/StatusUIKitCard.swift similarity index 99% rename from iosApp/flare/UI/Component/Status/StatusUIKitCard.swift rename to appleApp/ios/UI/Component/Status/StatusUIKitCard.swift index 3bf538f470..8cfd2d1b8d 100644 --- a/iosApp/flare/UI/Component/Status/StatusUIKitCard.swift +++ b/appleApp/ios/UI/Component/Status/StatusUIKitCard.swift @@ -1,5 +1,6 @@ import UIKit import KotlinSharedUI +import FlareAppleUI // MARK: - StatusCardUIView // Mirrors StatusCardView.swift: media on top, title (2 lines) + desc (2 lines) diff --git a/iosApp/flare/UI/Component/Status/StatusUIKitLeaves.swift b/appleApp/ios/UI/Component/Status/StatusUIKitLeaves.swift similarity index 95% rename from iosApp/flare/UI/Component/Status/StatusUIKitLeaves.swift rename to appleApp/ios/UI/Component/Status/StatusUIKitLeaves.swift index ea48192c71..5290c92493 100644 --- a/iosApp/flare/UI/Component/Status/StatusUIKitLeaves.swift +++ b/appleApp/ios/UI/Component/Status/StatusUIKitLeaves.swift @@ -1,6 +1,8 @@ import UIKit import Kingfisher import KotlinSharedUI +import FlareAppleCore +import AppleFontAwesome // MARK: - AvatarUIView // Mirrors AvatarView.swift (NetworkImage + clipShape by avatarShape). @@ -124,23 +126,23 @@ final class StatusVisibilityImageView: UIImageView { required init(coder: NSCoder) { fatalError("init(coder:) not supported") } func set(visibility: UiTimelineV2.PostVisibility) { - let name: String + let icon: FontAwesomeIcon switch visibility { - case .public: name = "fa-globe" - case .home: name = "fa-lock-open" - case .followers: name = "fa-lock" - case .specified: name = "fa-at" - case .channel: name = "fa-tv" - default: name = "fa-globe" + case .public: icon = .globe + case .home: icon = .lockOpen + case .followers: icon = .lock + case .specified: icon = .at + case .channel: icon = .tv + default: icon = .globe } - image = UIImage(named: name) + image = UIImage(fontAwesome: icon) } } // MARK: - TranslateStatusStateView // Mirrors TranslateStatusComponent.swift — language icon + state icon/spinner. final class TranslateStatusStateView: UIView, ManualLayoutMeasurable, TimelineHeightProviding { - private let langIcon = UIImageView(image: UIImage(named: "fa-language")) + private let langIcon = UIImageView(image: UIImage(fontAwesome: .language)) private let stateIcon = UIImageView() private let spinner = UIActivityIndicatorView(style: .medium) @@ -158,7 +160,7 @@ final class TranslateStatusStateView: UIView, ManualLayoutMeasurable, TimelineHe func set(state: TranslationDisplayState) { switch state { case .failed: - stateIcon.image = UIImage(named: "fa-circle-exclamation") + stateIcon.image = UIImage(fontAwesome: .circleExclamation) stateIcon.isHidden = false spinner.stopAnimating() spinner.isHidden = true @@ -480,7 +482,7 @@ final class StatusTopEndView: UIView, ManualLayoutMeasurable, TimelineHeightProv translation.setContentHuggingPriority(.required, for: .horizontal) translation.setContentCompressionResistancePriority(.required, for: .horizontal) insightButton.tintColor = .secondaryLabel - insightButton.setImage(UIImage(named: "fa-robot"), for: .normal) + insightButton.setImage(UIImage(fontAwesome: .robot), for: .normal) insightButton.imageView?.contentMode = .scaleAspectFit insightButton.accessibilityLabel = String(localized: "status_insight_title") insightButton.addAction( @@ -576,17 +578,17 @@ final class StatusTopEndView: UIView, ManualLayoutMeasurable, TimelineHeightProv if showPlatformLogo { platformLogo.isHidden = false - let name: String + let icon: FontAwesomeIcon? switch post.platformType { - case .mastodon: name = "fa-mastodon" - case .misskey: name = "fa-misskey" - case .bluesky: name = "fa-bluesky" - case .xQt: name = "fa-x-twitter" - case .vvo: name = "fa-weibo" - case .nostr: name = "fa-nostr" - default: name = "" + case .mastodon: icon = .mastodon + case .misskey: icon = .misskey + case .bluesky: icon = .bluesky + case .xQt: icon = .xTwitter + case .vvo: icon = .weibo + case .nostr: icon = .nostr + default: icon = nil } - platformLogo.image = UIImage(named: name) + platformLogo.image = icon.flatMap { UIImage(fontAwesome: $0) } } else { platformLogo.isHidden = true } diff --git a/iosApp/flare/UI/Component/Status/StatusUIKitMedia.swift b/appleApp/ios/UI/Component/Status/StatusUIKitMedia.swift similarity index 99% rename from iosApp/flare/UI/Component/Status/StatusUIKitMedia.swift rename to appleApp/ios/UI/Component/Status/StatusUIKitMedia.swift index f764263a6f..9e14e752ba 100644 --- a/iosApp/flare/UI/Component/Status/StatusUIKitMedia.swift +++ b/appleApp/ios/UI/Component/Status/StatusUIKitMedia.swift @@ -2,6 +2,8 @@ import UIKit import Kingfisher import KotlinSharedUI import SwiftUI +import FlareAppleUI +import AppleFontAwesome struct TimelineVideoAutoplayCandidate { let id: String @@ -75,7 +77,7 @@ final class MediaUIView: UIView { return v }() private let playBadge: UIImageView = { - let iv = UIImageView(image: UIImage(named: "fa-circle-play")) + let iv = UIImageView(image: UIImage(fontAwesome: .circlePlay)) iv.contentMode = .scaleAspectFit iv.tintColor = .white iv.translatesAutoresizingMaskIntoConstraints = false @@ -222,7 +224,7 @@ final class MediaUIView: UIView { switch state { case .idle: - playBadge.image = UIImage(named: "fa-circle-play") + playBadge.image = UIImage(fontAwesome: .circlePlay) playBadge.isHidden = false case .loading: loadingIndicator.isHidden = false @@ -650,7 +652,7 @@ final class StatusMediaUIView: UIView, TimelineHeightProviding { var cfg = UIButton.Configuration.bordered() if isBlurred { cfg.title = String(localized: "sensitive_button_show") - cfg.image = UIImage(named: "fa-eye") + cfg.image = UIImage(fontAwesome: .eye) cfg.imagePlacement = .leading cfg.baseForegroundColor = .white toggleButton.configuration = cfg @@ -661,7 +663,7 @@ final class StatusMediaUIView: UIView, TimelineHeightProviding { ] } else { cfg.title = nil - cfg.image = UIImage(named: "fa-eye-slash") + cfg.image = UIImage(fontAwesome: .eyeSlash) toggleButton.configuration = cfg toggleButtonPositionConstraints = [ toggleButton.topAnchor.constraint(equalTo: topAnchor, constant: 12), @@ -881,7 +883,7 @@ final class StatusMediaContentUIView: UIView, TimelineHeightProviding { var cfg = UIButton.Configuration.bordered() cfg.title = String(localized: "show_media_button") - cfg.image = UIImage(named: "fa-image") + cfg.image = UIImage(fontAwesome: .image) cfg.imagePlacement = .leading cfg.imagePadding = 4 showButton.configuration = cfg diff --git a/iosApp/flare/UI/Component/Status/StatusUIKitPoll.swift b/appleApp/ios/UI/Component/Status/StatusUIKitPoll.swift similarity index 100% rename from iosApp/flare/UI/Component/Status/StatusUIKitPoll.swift rename to appleApp/ios/UI/Component/Status/StatusUIKitPoll.swift diff --git a/iosApp/flare/UI/Component/Status/StatusUIKitReactions.swift b/appleApp/ios/UI/Component/Status/StatusUIKitReactions.swift similarity index 100% rename from iosApp/flare/UI/Component/Status/StatusUIKitReactions.swift rename to appleApp/ios/UI/Component/Status/StatusUIKitReactions.swift diff --git a/iosApp/flare/UI/Component/Status/StatusUIKitView.swift b/appleApp/ios/UI/Component/Status/StatusUIKitView.swift similarity index 99% rename from iosApp/flare/UI/Component/Status/StatusUIKitView.swift rename to appleApp/ios/UI/Component/Status/StatusUIKitView.swift index 1fd7660931..1345a97a59 100644 --- a/iosApp/flare/UI/Component/Status/StatusUIKitView.swift +++ b/appleApp/ios/UI/Component/Status/StatusUIKitView.swift @@ -1,6 +1,8 @@ import UIKit import SwiftUI import KotlinSharedUI +import FlareAppleCore +import AppleFontAwesome /// UIKit port of the SwiftUI `StatusView`. /// @@ -1157,7 +1159,7 @@ final class StatusUIKitView: UIView, UIGestureRecognizerDelegate, ManualLayoutMe // MARK: - ReplyToRowView (replaces replyToRow UIStackView) private final class ReplyToRowView: UIView, ManualLayoutMeasurable, TimelineHeightProviding { - private let icon = UIImageView(image: UIImage(named: "fa-reply")) + private let icon = UIImageView(image: UIImage(fontAwesome: .reply)) private let label = UILabel() private static let iconSize: CGFloat = 12 private static let spacing: CGFloat = 4 @@ -1221,7 +1223,7 @@ private final class ReplyToRowView: UIView, ManualLayoutMeasurable, TimelineHeig // MARK: - SourceChannelRowView (replaces sourceChannelRow UIStackView) private final class SourceChannelRowView: UIView, ManualLayoutMeasurable, TimelineHeightProviding { - private let icon = UIImageView(image: UIImage(named: "fa-tv")) + private let icon = UIImageView(image: UIImage(fontAwesome: .tv)) private let label = UILabel() private static let iconSize: CGFloat = 12 private static let spacing: CGFloat = 4 diff --git a/iosApp/flare/UI/Component/Status/TimelinePlaceholderUIView.swift b/appleApp/ios/UI/Component/Status/TimelinePlaceholderUIView.swift similarity index 100% rename from iosApp/flare/UI/Component/Status/TimelinePlaceholderUIView.swift rename to appleApp/ios/UI/Component/Status/TimelinePlaceholderUIView.swift diff --git a/iosApp/flare/UI/Component/Status/TimelineUIView.swift b/appleApp/ios/UI/Component/Status/TimelineUIView.swift similarity index 100% rename from iosApp/flare/UI/Component/Status/TimelineUIView.swift rename to appleApp/ios/UI/Component/Status/TimelineUIView.swift diff --git a/iosApp/flare/UI/Component/Status/TimelineUserUIView.swift b/appleApp/ios/UI/Component/Status/TimelineUserUIView.swift similarity index 99% rename from iosApp/flare/UI/Component/Status/TimelineUserUIView.swift rename to appleApp/ios/UI/Component/Status/TimelineUserUIView.swift index 352c9bf43f..f931dc8471 100644 --- a/iosApp/flare/UI/Component/Status/TimelineUserUIView.swift +++ b/appleApp/ios/UI/Component/Status/TimelineUserUIView.swift @@ -1,5 +1,6 @@ import UIKit import KotlinSharedUI +import FlareAppleCore /// UIKit port of `TimelineUserView`. /// diff --git a/iosApp/flare/UI/Component/Status/UIKitStackDiff.swift b/appleApp/ios/UI/Component/Status/UIKitStackDiff.swift similarity index 100% rename from iosApp/flare/UI/Component/Status/UIKitStackDiff.swift rename to appleApp/ios/UI/Component/Status/UIKitStackDiff.swift diff --git a/iosApp/flare/UI/Component/Status/UserListUIView.swift b/appleApp/ios/UI/Component/Status/UserListUIView.swift similarity index 99% rename from iosApp/flare/UI/Component/Status/UserListUIView.swift rename to appleApp/ios/UI/Component/Status/UserListUIView.swift index f8a724da47..fffa597e2d 100644 --- a/iosApp/flare/UI/Component/Status/UserListUIView.swift +++ b/appleApp/ios/UI/Component/Status/UserListUIView.swift @@ -1,5 +1,6 @@ import UIKit import KotlinSharedUI +import FlareAppleCore /// UIKit port of `UserListView`. /// diff --git a/iosApp/flare/UI/Component/SwiftInAppNotification.swift b/appleApp/ios/UI/Component/SwiftInAppNotification.swift similarity index 91% rename from iosApp/flare/UI/Component/SwiftInAppNotification.swift rename to appleApp/ios/UI/Component/SwiftInAppNotification.swift index 60bd695a81..3a007cc1ac 100644 --- a/iosApp/flare/UI/Component/SwiftInAppNotification.swift +++ b/appleApp/ios/UI/Component/SwiftInAppNotification.swift @@ -1,3 +1,4 @@ +import AppleFontAwesome import KotlinSharedUI import Drops import Foundation @@ -14,7 +15,7 @@ class SwiftInAppNotification: InAppNotification { Drops.show( .init( title: .init(localized: "notification_compose_error"), - icon: .faCircleExclamation + icon: UIImage(fontAwesome: .circleExclamation) ) ) @@ -45,7 +46,7 @@ class SwiftInAppNotification: InAppNotification { Drops.show( .init( title: .init(localized: "notification_compose_success"), - icon: .faCircleCheck + icon: UIImage(fontAwesome: .circleCheck) ) ) case .loginExpired: diff --git a/appleApp/ios/UI/Component/TabIcon.swift b/appleApp/ios/UI/Component/TabIcon.swift new file mode 100644 index 0000000000..d8a5619b32 --- /dev/null +++ b/appleApp/ios/UI/Component/TabIcon.swift @@ -0,0 +1,144 @@ +import SwiftUI +import KotlinSharedUI +import FlareAppleCore +import FlareAppleUI +import AppleFontAwesome + +public struct TabTitle: View { + private let title: Any + + public init(title: Any) { + self.title = title + } + + public var body: some View { + Text(String(describing: title)) + } +} + +public struct TimelineTabTitle: View { + private let title: UiText + + public init(title: UiText) { + self.title = title + } + + public var body: some View { + Text(title.text) + } +} + +public struct TabIcon: View { + private let icon: IconType + private let size: CGFloat + private let iconOnly: Bool + + public init( + icon: IconType, + size: CGFloat = 20, + iconOnly: Bool = false + ) { + self.icon = icon + self.size = size + self.iconOnly = iconOnly + } + + public var body: some View { + switch onEnum(of: icon) { + case .material(let material): + MaterialTabIcon(icon: material.icon) + .frame(width: size, height: size) + case .avatar(let avatar): + AvatarTabIcon(userKey: avatar.accountKey, accountType: AccountType.Specific(accountKey: avatar.accountKey)) + .frame(width: size, height: size) + case .url(let url): + NetworkImage(data: url.url) + .frame(width: size, height: size) + case .mixed(let mixed): + if iconOnly { + MaterialTabIcon(icon: mixed.icon) + .frame(width: size, height: size) + } else { + ZStack( + alignment: .bottomTrailing + ) { + AvatarTabIcon(userKey: mixed.accountKey, accountType: AccountType.Specific(accountKey: mixed.accountKey)) + .frame(width: size, height: size) + MaterialTabIcon(icon: mixed.icon) + .padding(2) + .background(Color.white) + .foregroundStyle(Color.black) + .clipShape(.circle) + .frame(width: size / 2, height: size / 2) + } + .frame(width: size, height: size) + } + case .favIcon(let favIcon): + FavTabIcon(host: favIcon.host) + .frame(width: size, height: size) + } + } +} + +public extension TabIcon { + init( + tabItem: UiTimelineTabItem, + size: CGFloat = 20, + iconOnly: Bool = false + ) { + self.init(icon: tabItem.icon, size: size, iconOnly: iconOnly) + } +} + +public struct MaterialTabIcon: View { + private let icon: UiIcon + + public init(icon: UiIcon) { + self.icon = icon + } + + public var body: some View { + Image(fontAwesome: icon.fontAwesomeIcon) + .resizable() + .scaledToFit() + } +} + +public struct AvatarTabIcon: View { + + @StateObject private var presenter: KotlinPresenter + + public init(userKey: MicroBlogKey, accountType: AccountType) { + self._presenter = .init(wrappedValue: .init(presenter: UserPresenter(accountType: accountType, userKey: userKey))) + } + + public var body: some View { + StateView(state: presenter.state.user) { user in + AvatarView(data: user.avatar?.url, customHeader: user.avatar?.customHeaders) + } loadingContent: { + Image(fontAwesome: .globe) + .resizable() + .scaledToFit() + .redacted(reason: .placeholder) + } + } +} + +public struct FavTabIcon: View { + @StateObject private var presenter: KotlinPresenter> + + public init(host: String) { + self._presenter = .init(wrappedValue: .init(presenter: FavIconPresenter(host: host))) + } + + public var body: some View { + StateView(state: presenter.state) { url in + NetworkImage(data: .init(url)) + } loadingContent: { + Image(fontAwesome: .globe) + .resizable() + .scaledToFit() + .redacted(reason: .placeholder) + } + } +} diff --git a/iosApp/flare/UI/Component/TimelinePagingView.swift b/appleApp/ios/UI/Component/TimelinePagingView.swift similarity index 52% rename from iosApp/flare/UI/Component/TimelinePagingView.swift rename to appleApp/ios/UI/Component/TimelinePagingView.swift index 7e61e9524c..404454b525 100644 --- a/iosApp/flare/UI/Component/TimelinePagingView.swift +++ b/appleApp/ios/UI/Component/TimelinePagingView.swift @@ -1,63 +1,6 @@ import SwiftUI import KotlinSharedUI - -struct TimelinePagingView: View { - let data: PagingState - let detailStatusKey: MicroBlogKey? - var body: some View { - PagingView(data: data) { - ListEmptyView() - } errorContent: { error, retry in - ListErrorView(error: error) { - retry() - } - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) - } loadingContent: { index, totalCount in - AdaptiveTimelineCard(index: index, totalCount: totalCount) { - TimelinePlaceholderView() - .padding(.horizontal) - .padding(.vertical, 12) - } - } successContent: { item, index, totalCount in - AdaptiveTimelineCard(index: index, totalCount: totalCount) { - TimelineView(data: item, detailStatusKey: detailStatusKey) - .padding(.horizontal) - .padding(.vertical, 12) - } - } - } -} - -extension TimelinePagingView { - init(data: PagingState) { - self.data = data - self.detailStatusKey = nil - } -} - -struct TimelineData: Identifiable, Hashable { - let id: String - let data: UiTimelineV2? - let index: Int -} - -struct TimelineCollection: @MainActor RandomAccessCollection { - let data: PagingStateSuccess - public var startIndex: Int { 0 } - public var endIndex: Int { Int(data.itemCount) } - - public func index(after index: Int) -> Int { index + 1 } - public func index(before index: Int) -> Int { index - 1 } - public func index(_ index: Int, offsetBy distance: Int) -> Int { index + distance } - public func distance(from start: Int, to end: Int) -> Int { end - start } - - public subscript(position: Int) -> TimelineData { - let item = data.peek(index: Int32(position)) - return TimelineData(id: item?.itemKey ?? "\(position)", data: item, index: position) - } - - public var count: Int { Int(data.itemCount) } -} +import FlareAppleUI struct TimelinePagingContent: View { @Environment(\.horizontalSizeClass) private var horizontalSizeClass diff --git a/iosApp/flare/UI/Component/TimelinePresentationEditor.swift b/appleApp/ios/UI/Component/TimelinePresentationEditor.swift similarity index 96% rename from iosApp/flare/UI/Component/TimelinePresentationEditor.swift rename to appleApp/ios/UI/Component/TimelinePresentationEditor.swift index 7d951ea046..5cf7431ef0 100644 --- a/iosApp/flare/UI/Component/TimelinePresentationEditor.swift +++ b/appleApp/ios/UI/Component/TimelinePresentationEditor.swift @@ -1,27 +1,28 @@ import SwiftUI import KotlinSharedUI +import AppleFontAwesome -struct TimelinePresentationEditor: View { +public struct TimelinePresentationEditor: View { @Binding var text: String @Binding var icon: IconType - let availableIcons: [IconType] - let withAvatar: Bool - let canUseAvatar: Bool - let onWithAvatarChange: (Bool) -> Void + private let availableIcons: [IconType] + private let withAvatar: Bool + private let canUseAvatar: Bool + private let onWithAvatarChange: (Bool) -> Void @Binding var enabled: Bool @Binding var filterConfig: TimelineFilterConfig - let onEditFilter: () -> Void - let showEnabled: Bool - let showFilter: Bool - let showAppearanceOverrides: Bool - let timelineAppearance: TimelineAppearance + private let onEditFilter: () -> Void + private let showEnabled: Bool + private let showFilter: Bool + private let showAppearanceOverrides: Bool + private let timelineAppearance: TimelineAppearance @Binding var appearancePatch: AppearancePatch - let behaviorContent: AnyView? - let titlePlaceholder: LocalizedStringKey + private let behaviorContent: AnyView? + private let titlePlaceholder: LocalizedStringKey @State private var showIconPicker = false - init( + public init( text: Binding, icon: Binding, availableIcons: [IconType], @@ -57,7 +58,7 @@ struct TimelinePresentationEditor: View { self.titlePlaceholder = titlePlaceholder } - var body: some View { + public var body: some View { Group { TimelinePresentationHeaderEditor( text: $text, @@ -156,16 +157,16 @@ private struct TimelineFilterSettingsItem: View { } } -struct TimelineFilterSheet: View { +public struct TimelineFilterSheet: View { @State private var selectedKinds: Set @State private var selectedContents: Set - let onCancel: () -> Void - let onConfirm: (TimelineFilterConfig) -> Void + private let onCancel: () -> Void + private let onConfirm: (TimelineFilterConfig) -> Void private let kindOptions: [TimelinePostKind] = [.reply, .repost, .quote] private let contentOptions: [TimelinePostContent] = [.text, .image, .video] - init( + public init( initialFilterConfig: TimelineFilterConfig, onCancel: @escaping () -> Void, onConfirm: @escaping (TimelineFilterConfig) -> Void @@ -177,7 +178,7 @@ struct TimelineFilterSheet: View { self._selectedContents = State(initialValue: Set(contentOptions.filter { !current.excludedContents.contains($0) })) } - var body: some View { + public var body: some View { Form { Section { ForEach(kindOptions, id: \.self) { kind in @@ -224,7 +225,7 @@ struct TimelineFilterSheet: View { Label { Text("Close") } icon: { - Image("fa-xmark") + Image(fontAwesome: .xmark) } } } @@ -240,7 +241,7 @@ struct TimelineFilterSheet: View { Label { Text("Done") } icon: { - Image("fa-check") + Image(fontAwesome: .check) } } } diff --git a/iosApp/flare/UI/Component/UIKitAppearance.swift b/appleApp/ios/UI/Component/UIKitAppearance.swift similarity index 99% rename from iosApp/flare/UI/Component/UIKitAppearance.swift rename to appleApp/ios/UI/Component/UIKitAppearance.swift index efafc1672b..e2658f4305 100644 --- a/iosApp/flare/UI/Component/UIKitAppearance.swift +++ b/appleApp/ios/UI/Component/UIKitAppearance.swift @@ -1,5 +1,6 @@ import KotlinSharedUI import UIKit +import FlareAppleUI private func contentSizeCategory(fontSizeDiff: Float) -> UIContentSizeCategory { switch Int(fontSizeDiff.rounded()) { diff --git a/iosApp/flare/UI/Component/UiListView.swift b/appleApp/ios/UI/Component/UiListView.swift similarity index 93% rename from iosApp/flare/UI/Component/UiListView.swift rename to appleApp/ios/UI/Component/UiListView.swift index f7fbd9244f..8982c83de3 100644 --- a/iosApp/flare/UI/Component/UiListView.swift +++ b/appleApp/ios/UI/Component/UiListView.swift @@ -1,9 +1,15 @@ import SwiftUI import KotlinSharedUI +import FlareAppleUI -struct UiListView: View { - let data: UiList - var body: some View { +public struct UiListView: View { + private let data: UiList + + public init(data: UiList) { + self.data = data + } + + public var body: some View { switch data { case let list as UiList.List: UiListRow(data: list) @@ -120,8 +126,10 @@ private struct UiChannelRow: View { } } -struct UiListPlaceholder: View { - var body: some View { +public struct UiListPlaceholder: View { + public init() {} + + public var body: some View { VStack( alignment: .leading, spacing: 8 diff --git a/iosApp/flare/UI/Component/UiRssView.swift b/appleApp/ios/UI/Component/UiRssView.swift similarity index 72% rename from iosApp/flare/UI/Component/UiRssView.swift rename to appleApp/ios/UI/Component/UiRssView.swift index 06514abe62..351d8794eb 100644 --- a/iosApp/flare/UI/Component/UiRssView.swift +++ b/appleApp/ios/UI/Component/UiRssView.swift @@ -1,9 +1,16 @@ import SwiftUI import KotlinSharedUI +import FlareAppleUI +import AppleFontAwesome -struct UiRssView: View { - let data: UiRssSource - var body: some View { +public struct UiRssView: View { + private let data: UiRssSource + + public init(data: UiRssSource) { + self.data = data + } + + public var body: some View { VStack( alignment: .leading, spacing: 8 @@ -19,7 +26,7 @@ struct UiRssView: View { NetworkImage(data: favIcon) .frame(width: 24, height: 24) } else { - Image("fa-square-rss") + Image(fontAwesome: .squareRss) } } Text(data.url) diff --git a/iosApp/flare/UI/Component/ViewBox.swift b/appleApp/ios/UI/Component/ViewBox.swift similarity index 82% rename from iosApp/flare/UI/Component/ViewBox.swift rename to appleApp/ios/UI/Component/ViewBox.swift index ff7ab6c601..012af86292 100644 --- a/iosApp/flare/UI/Component/ViewBox.swift +++ b/appleApp/ios/UI/Component/ViewBox.swift @@ -1,13 +1,13 @@ import SwiftUI -struct ViewBox: View { - var content: Content - - init(@ViewBuilder content: () -> Content) { +public struct ViewBox: View { + public var content: Content + + public init(@ViewBuilder content: () -> Content) { self.content = content() } - - var body: some View { + + public var body: some View { GeometryReader { geo in content .fixedSize() diff --git a/iosApp/flare/UI/FlareRoot.swift b/appleApp/ios/UI/FlareRoot.swift similarity index 92% rename from iosApp/flare/UI/FlareRoot.swift rename to appleApp/ios/UI/FlareRoot.swift index f33efe3114..ad1f09e928 100644 --- a/iosApp/flare/UI/FlareRoot.swift +++ b/appleApp/ios/UI/FlareRoot.swift @@ -1,6 +1,9 @@ import SwiftUI +import FlareAppleUI import KotlinSharedUI +import FlareAppleCore import SwiftUIBackports +import AppleFontAwesome @available(iOS 18.0, *) struct FlareRoot: View { @@ -25,7 +28,7 @@ struct FlareRoot: View { Label { Text(homeTabTitle(tab)) } icon: { - Image(homeTabIconName(tab)) + Image(fontAwesome: homeTabIcon(tab)) } .adaptiveLabelStyle(globalAppearance.showBottomBarLabels || horizontalSizeClass == .regular) } @@ -77,7 +80,7 @@ struct FlareRoot: View { Label { Text(tab.title.text) } icon: { - Image(tab.icon.imageName) + Image(fontAwesome: tab.icon.fontAwesomeIcon) } } .tabPlacement(.sidebarOnly) @@ -91,7 +94,7 @@ struct FlareRoot: View { Label { Text(route.title) } icon: { - Image(route.iconName) + Image(fontAwesome: route.icon) } } .tabPlacement(.sidebarOnly) @@ -126,7 +129,7 @@ struct BackportFlareRoot: View { Label { Text(homeTabTitle(tab)) } icon: { - Image(homeTabIconName(tab)) + Image(fontAwesome: homeTabIcon(tab)) } .adaptiveLabelStyle(globalAppearance.showBottomBarLabels) } @@ -167,14 +170,14 @@ private func homeTabTitle(_ tab: HomeTabsPresenterStateHomeTabs) -> LocalizedStr } } -private func homeTabIconName(_ tab: HomeTabsPresenterStateHomeTabs) -> String { +private func homeTabIcon(_ tab: HomeTabsPresenterStateHomeTabs) -> FontAwesomeIcon { switch tab { case .notifications: - return "fa-bell" + return .bell case .discover: - return "fa-magnifying-glass" + return .magnifyingGlass case .home: - return "fa-house" + return .house } } @@ -243,18 +246,18 @@ private enum SecondarySidebarStaticRoute: CaseIterable { } } - var iconName: String { + var icon: FontAwesomeIcon { switch self { case .drafts: - return "fa-pen-to-square" + return .penToSquare case .rssManagement: - return "fa-square-rss" + return .squareRss case .localHistory: - return "fa-clock-rotate-left" + return .clockRotateLeft case .agentHistory: - return "fa-robot" + return .robot case .settings: - return "fa-gear" + return .gear } } } diff --git a/iosApp/flare/UI/FlareTheme.swift b/appleApp/ios/UI/FlareTheme.swift similarity index 78% rename from iosApp/flare/UI/FlareTheme.swift rename to appleApp/ios/UI/FlareTheme.swift index dab2195a49..21585be900 100644 --- a/iosApp/flare/UI/FlareTheme.swift +++ b/appleApp/ios/UI/FlareTheme.swift @@ -2,6 +2,8 @@ import SwiftUI import KotlinSharedUI import Foundation import Combine +import FlareAppleCore +import FlareAppleUI struct FlareTheme: View { @ViewBuilder let content: () -> Content @@ -96,34 +98,3 @@ private extension URL { return scheme == "http" || scheme == "https" } } - -private struct GlobalAppearanceKey: EnvironmentKey { - static let defaultValue = GlobalAppearance.companion.Default -} -private struct TimelineAppearanceKey: EnvironmentKey { - static let defaultValue = TimelineAppearance.companion.Default -} -private struct AiConfigKey: EnvironmentKey { - static let defaultValue = AppSettings.AiConfig.companion.default -} -private struct TranslateConfigKey: EnvironmentKey { - static let defaultValue = AppSettings.TranslateConfig() -} -extension EnvironmentValues { - var globalAppearance: GlobalAppearance { - get { self[GlobalAppearanceKey.self] } - set { self[GlobalAppearanceKey.self] = newValue } - } - var timelineAppearance: TimelineAppearance { - get { self[TimelineAppearanceKey.self] } - set { self[TimelineAppearanceKey.self] = newValue } - } - var aiConfig: AppSettings.AiConfig { - get { self[AiConfigKey.self] } - set { self[AiConfigKey.self] = newValue } - } - var translateConfig: AppSettings.TranslateConfig { - get { self[TranslateConfigKey.self] } - set { self[TranslateConfigKey.self] = newValue } - } -} diff --git a/iosApp/flare/UI/Model/Extension.swift b/appleApp/ios/UI/Model/Extension.swift similarity index 100% rename from iosApp/flare/UI/Model/Extension.swift rename to appleApp/ios/UI/Model/Extension.swift diff --git a/iosApp/flare/UI/Route/Route.swift b/appleApp/ios/UI/Route/Route.swift similarity index 99% rename from iosApp/flare/UI/Route/Route.swift rename to appleApp/ios/UI/Route/Route.swift index b393e11cbd..c535886ae3 100644 --- a/iosApp/flare/UI/Route/Route.swift +++ b/appleApp/ios/UI/Route/Route.swift @@ -1,5 +1,7 @@ import SwiftUI +import FlareAppleUI import KotlinSharedUI +import FlareAppleCore enum Route: Hashable, Identifiable { var id: Int { diff --git a/iosApp/flare/UI/Route/Router.swift b/appleApp/ios/UI/Route/Router.swift similarity index 99% rename from iosApp/flare/UI/Route/Router.swift rename to appleApp/ios/UI/Route/Router.swift index 47d4e9d740..864525cff4 100644 --- a/iosApp/flare/UI/Route/Router.swift +++ b/appleApp/ios/UI/Route/Router.swift @@ -2,6 +2,7 @@ import SwiftUI import KotlinSharedUI import LazyPager import Combine +import FlareAppleCore struct Router: View { @Environment(\.openURL) private var openURL diff --git a/iosApp/flare/UI/Screen/AboutScreen.swift b/appleApp/ios/UI/Screen/AboutScreen.swift similarity index 95% rename from iosApp/flare/UI/Screen/AboutScreen.swift rename to appleApp/ios/UI/Screen/AboutScreen.swift index a6fede3480..50385b3d95 100644 --- a/iosApp/flare/UI/Screen/AboutScreen.swift +++ b/appleApp/ios/UI/Screen/AboutScreen.swift @@ -1,5 +1,6 @@ import SwiftUI import UIKit +import AppleFontAwesome struct AboutScreen: View { @Environment(\.openURL) private var openURL @@ -87,7 +88,7 @@ private struct AboutLinkRow: View { var body: some View { HStack(spacing: 14) { - Image(item.iconName) + Image(fontAwesome: item.icon) .resizable() .scaledToFit() .foregroundStyle(.tint) @@ -118,7 +119,7 @@ private struct AboutLink: Identifiable { let id: String let titleKey: LocalizedStringKey let subtitleKey: LocalizedStringKey - let iconName: String + let icon: FontAwesomeIcon let url: URL } @@ -128,35 +129,35 @@ private extension AboutLink { id: "source-code", titleKey: "settings_about_source_code", subtitleKey: "https://github.com/DimensionDev/Flare", - iconName: "fa-github", + icon: .github, url: URL(string: "https://github.com/DimensionDev/Flare")! ), AboutLink( id: "telegram", titleKey: "settings_about_telegram", subtitleKey: "settings_about_telegram_description", - iconName: "fa-telegram", + icon: .telegram, url: URL(string: "https://t.me/+VZ63fqNQXIA0MzVl")! ), AboutLink( id: "discord", titleKey: "settings_about_discord", subtitleKey: "settings_about_discord_description", - iconName: "fa-discord", + icon: .discord, url: URL(string: "https://discord.gg/De9NhXBryT")! ), AboutLink( id: "localization", titleKey: "settings_about_localization", subtitleKey: "settings_about_localization_description", - iconName: "fa-language", + icon: .language, url: URL(string: "https://crowdin.com/project/flareapp")! ), AboutLink( id: "privacy-policy", titleKey: "settings_privacy_policy", subtitleKey: "https://legal.mask.io/maskbook", - iconName: "fa-lock", + icon: .lock, url: URL(string: "https://legal.mask.io/maskbook/")! ) ] diff --git a/iosApp/flare/UI/Screen/AccountManagementScreen.swift b/appleApp/ios/UI/Screen/AccountManagementScreen.swift similarity index 92% rename from iosApp/flare/UI/Screen/AccountManagementScreen.swift rename to appleApp/ios/UI/Screen/AccountManagementScreen.swift index e270bdb52d..e30786dc1b 100644 --- a/iosApp/flare/UI/Screen/AccountManagementScreen.swift +++ b/appleApp/ios/UI/Screen/AccountManagementScreen.swift @@ -1,5 +1,8 @@ import SwiftUI import KotlinSharedUI +import FlareAppleCore +import FlareAppleUI +import AppleFontAwesome struct AccountManagementScreen: View { @StateObject private var presenter = KotlinPresenter(presenter: AccountManagementPresenter()) @@ -31,7 +34,7 @@ struct AccountManagementScreen: View { Label { Text("Manage relays") } icon: { - Image("fa-list") + Image(fontAwesome: .list) } } } @@ -44,7 +47,7 @@ struct AccountManagementScreen: View { Label { Text("logout_title") } icon: { - Image("fa-trash") + Image(fontAwesome: .trash) } } } @@ -54,7 +57,7 @@ struct AccountManagementScreen: View { Label { Text("Manage relays") } icon: { - Image("fa-list") + Image(fontAwesome: .list) } } .tint(.accentColor) @@ -68,7 +71,7 @@ struct AccountManagementScreen: View { Label { Text("logout_title") } icon: { - Image("fa-trash") + Image(fontAwesome: .trash) } } } @@ -80,7 +83,7 @@ struct AccountManagementScreen: View { Label { Text("Manage relays") } icon: { - Image("fa-square-rss") + Image(fontAwesome: .squareRss) } } } @@ -93,7 +96,7 @@ struct AccountManagementScreen: View { Label { Text("logout_title") } icon: { - Image("fa-trash") + Image(fontAwesome: .trash) } } } @@ -103,7 +106,7 @@ struct AccountManagementScreen: View { Label { Text("Manage relays") } icon: { - Image("fa-square-rss") + Image(fontAwesome: .squareRss) } } .tint(.accentColor) @@ -117,7 +120,7 @@ struct AccountManagementScreen: View { Label { Text("logout_title") } icon: { - Image("fa-trash") + Image(fontAwesome: .trash) } } } @@ -160,7 +163,7 @@ struct AccountManagementScreen: View { Label { Text("login_title") } icon: { - Image("fa-plus") + Image(fontAwesome: .plus) } } } diff --git a/iosApp/flare/UI/Screen/AgentChatHistoryScreen.swift b/appleApp/ios/UI/Screen/AgentChatHistoryScreen.swift similarity index 91% rename from iosApp/flare/UI/Screen/AgentChatHistoryScreen.swift rename to appleApp/ios/UI/Screen/AgentChatHistoryScreen.swift index c7d73b436c..ad1c6b432b 100644 --- a/iosApp/flare/UI/Screen/AgentChatHistoryScreen.swift +++ b/appleApp/ios/UI/Screen/AgentChatHistoryScreen.swift @@ -1,5 +1,7 @@ import SwiftUI import KotlinSharedUI +import FlareAppleCore +import AppleFontAwesome struct AgentChatHistoryScreen: View { @StateObject private var presenter = KotlinPresenter(presenter: AgentChatHistoryPresenter()) @@ -11,7 +13,7 @@ struct AgentChatHistoryScreen: View { Label { Text("agent_history_empty") } icon: { - Image("fa-robot") + Image(fontAwesome: .robot) } } } else { @@ -37,7 +39,7 @@ struct AgentChatHistoryScreen: View { .toolbar { ToolbarItem(placement: .primaryAction) { NavigationLink(value: Route.agentChat(Route.newGenericChatConversationId(), nil)) { - Image("fa-plus") + Image(fontAwesome: .plus) } .accessibilityLabel(Text("agent_chat_title")) } diff --git a/iosApp/flare/UI/Screen/AgentChatScreen.swift b/appleApp/ios/UI/Screen/AgentChatScreen.swift similarity index 99% rename from iosApp/flare/UI/Screen/AgentChatScreen.swift rename to appleApp/ios/UI/Screen/AgentChatScreen.swift index 1835fcfeb0..a507feddd2 100644 --- a/iosApp/flare/UI/Screen/AgentChatScreen.swift +++ b/appleApp/ios/UI/Screen/AgentChatScreen.swift @@ -1,5 +1,6 @@ import SwiftUI import KotlinSharedUI +import FlareAppleCore struct AgentChatScreen: View { @StateObject private var presenter: KotlinPresenter diff --git a/iosApp/flare/UI/Screen/AiConfigScreen.swift b/appleApp/ios/UI/Screen/AiConfigScreen.swift similarity index 98% rename from iosApp/flare/UI/Screen/AiConfigScreen.swift rename to appleApp/ios/UI/Screen/AiConfigScreen.swift index 866214b996..9e1e8f6e11 100644 --- a/iosApp/flare/UI/Screen/AiConfigScreen.swift +++ b/appleApp/ios/UI/Screen/AiConfigScreen.swift @@ -1,5 +1,8 @@ import SwiftUI import KotlinSharedUI +import FlareAppleCore +import FlareAppleUI +import AppleFontAwesome struct AiConfigScreen: View { @StateObject private var presenter = KotlinPresenter(presenter: AiConfigPresenter()) @@ -254,7 +257,7 @@ struct AiConfigScreen: View { Button { editingField = nil } label: { - Image("fa-xmark") + Image(fontAwesome: .xmark) } } ToolbarItem(placement: .confirmationAction) { @@ -262,7 +265,7 @@ struct AiConfigScreen: View { applyEdit(field: field, value: editingText) editingField = nil } label: { - Image("fa-check") + Image(fontAwesome: .check) } } } @@ -607,7 +610,7 @@ struct TranslationConfigScreen: View { Button { editingField = nil } label: { - Image("fa-xmark") + Image(fontAwesome: .xmark) } } ToolbarItem(placement: .confirmationAction) { @@ -615,7 +618,7 @@ struct TranslationConfigScreen: View { applyTranslationEdit(field: field, value: editingText) editingField = nil } label: { - Image("fa-check") + Image(fontAwesome: .check) } } } @@ -646,7 +649,7 @@ struct TranslationConfigScreen: View { Button { showExcludedLanguagesPicker = false } label: { - Image("fa-xmark") + Image(fontAwesome: .xmark) } } ToolbarItem(placement: .confirmationAction) { @@ -658,7 +661,7 @@ struct TranslationConfigScreen: View { ) showExcludedLanguagesPicker = false } label: { - Image("fa-check") + Image(fontAwesome: .check) } } } diff --git a/iosApp/flare/UI/Screen/AllFeedScreen.swift b/appleApp/ios/UI/Screen/AllFeedScreen.swift similarity index 97% rename from iosApp/flare/UI/Screen/AllFeedScreen.swift rename to appleApp/ios/UI/Screen/AllFeedScreen.swift index 1a68f971fc..d248d1b134 100644 --- a/iosApp/flare/UI/Screen/AllFeedScreen.swift +++ b/appleApp/ios/UI/Screen/AllFeedScreen.swift @@ -1,5 +1,7 @@ import SwiftUI +import FlareAppleUI @preconcurrency import KotlinSharedUI +import FlareAppleCore struct AllFeedScreen: View { @StateObject private var presenter: KotlinPresenter diff --git a/iosApp/flare/UI/Screen/AllListScreen.swift b/appleApp/ios/UI/Screen/AllListScreen.swift similarity index 91% rename from iosApp/flare/UI/Screen/AllListScreen.swift rename to appleApp/ios/UI/Screen/AllListScreen.swift index fa2d954d85..e0d7ec7159 100644 --- a/iosApp/flare/UI/Screen/AllListScreen.swift +++ b/appleApp/ios/UI/Screen/AllListScreen.swift @@ -1,5 +1,8 @@ import SwiftUI +import FlareAppleUI +import AppleFontAwesome @preconcurrency import KotlinSharedUI +import FlareAppleCore struct AllListScreen: View { @StateObject private var presenter: KotlinPresenter @@ -31,7 +34,7 @@ struct AllListScreen: View { Label { Text("list_edit_title") } icon: { - Image(.faPen) + Image(fontAwesome: .pen) } } } @@ -42,7 +45,7 @@ struct AllListScreen: View { Label { Text("delete") } icon: { - Image(.faTrash) + Image(fontAwesome: .trash) } } } @@ -53,7 +56,7 @@ struct AllListScreen: View { Label { Text("edit") } icon: { - Image(.faPen) + Image(fontAwesome: .pen) } } Button(role: .destructive) { @@ -62,7 +65,7 @@ struct AllListScreen: View { Label { Text("delete") } icon: { - Image(.faTrash) + Image(fontAwesome: .trash) } } } @@ -107,7 +110,7 @@ struct AllListScreen: View { Button { showCreateListSheet = true } label: { - Image(.faPlus) + Image(fontAwesome: .plus) } } } diff --git a/iosApp/flare/UI/Screen/AltTextEditSheet.swift b/appleApp/ios/UI/Screen/AltTextEditSheet.swift similarity index 100% rename from iosApp/flare/UI/Screen/AltTextEditSheet.swift rename to appleApp/ios/UI/Screen/AltTextEditSheet.swift diff --git a/iosApp/flare/UI/Screen/AntennasListScreen.swift b/appleApp/ios/UI/Screen/AntennasListScreen.swift similarity index 96% rename from iosApp/flare/UI/Screen/AntennasListScreen.swift rename to appleApp/ios/UI/Screen/AntennasListScreen.swift index 991675c715..f8b5084d25 100644 --- a/iosApp/flare/UI/Screen/AntennasListScreen.swift +++ b/appleApp/ios/UI/Screen/AntennasListScreen.swift @@ -1,5 +1,7 @@ import SwiftUI +import FlareAppleUI @preconcurrency import KotlinSharedUI +import FlareAppleCore struct AntennasListScreen: View { let accountType: AccountType diff --git a/iosApp/flare/UI/Screen/AppIconSettingsScreen.swift b/appleApp/ios/UI/Screen/AppIconSettingsScreen.swift similarity index 98% rename from iosApp/flare/UI/Screen/AppIconSettingsScreen.swift rename to appleApp/ios/UI/Screen/AppIconSettingsScreen.swift index 03f42686bc..d6631c8778 100644 --- a/iosApp/flare/UI/Screen/AppIconSettingsScreen.swift +++ b/appleApp/ios/UI/Screen/AppIconSettingsScreen.swift @@ -1,5 +1,6 @@ import SwiftUI import UIKit +import AppleFontAwesome struct AppIconSettingsScreen: View { @State private var currentIconName = UIApplication.shared.alternateIconName @@ -99,7 +100,7 @@ private struct AppIconGridItem: View { .padding(8) .background(.regularMaterial, in: Circle()) } else if isSelected { - Image(.faCheck) + Image(fontAwesome: .check) .font(.caption.weight(.bold)) .foregroundStyle(.white) .frame(width: 24, height: 24) diff --git a/iosApp/flare/UI/Screen/AppLogScreen.swift b/appleApp/ios/UI/Screen/AppLogScreen.swift similarity index 92% rename from iosApp/flare/UI/Screen/AppLogScreen.swift rename to appleApp/ios/UI/Screen/AppLogScreen.swift index fd521c9e95..74d21010bf 100644 --- a/iosApp/flare/UI/Screen/AppLogScreen.swift +++ b/appleApp/ios/UI/Screen/AppLogScreen.swift @@ -1,5 +1,7 @@ import SwiftUI import KotlinSharedUI +import FlareAppleCore +import AppleFontAwesome struct AppLogScreen: View { @StateObject private var presenter = KotlinPresenter(presenter: DevModePresenter()) @@ -29,14 +31,14 @@ struct AppLogScreen: View { Button { presenter.state.clear() } label: { - Image(.faTrash) + Image(fontAwesome: .trash) } } ToolbarItem { Button { exportedLogContent = presenter.state.printMessageToString() } label: { - Image(.faFloppyDisk) + Image(fontAwesome: .floppyDisk) } } } @@ -67,7 +69,7 @@ struct AppLogScreen: View { Button { selectedMessage = nil } label: { - Image(.faXmark) + Image(fontAwesome: .xmark) } } } diff --git a/iosApp/flare/UI/Screen/AppearanceDisplayScreen.swift b/appleApp/ios/UI/Screen/AppearanceDisplayScreen.swift similarity index 98% rename from iosApp/flare/UI/Screen/AppearanceDisplayScreen.swift rename to appleApp/ios/UI/Screen/AppearanceDisplayScreen.swift index 6d82640e35..30148f62fc 100644 --- a/iosApp/flare/UI/Screen/AppearanceDisplayScreen.swift +++ b/appleApp/ios/UI/Screen/AppearanceDisplayScreen.swift @@ -1,5 +1,7 @@ import SwiftUI +import FlareAppleUI import KotlinSharedUI +import FlareAppleCore struct AppearanceDisplayScreen: View { @StateObject private var statusPresenter = KotlinPresenter(presenter: AppearancePresenter()) diff --git a/iosApp/flare/UI/Screen/AppearanceLayoutScreen.swift b/appleApp/ios/UI/Screen/AppearanceLayoutScreen.swift similarity index 98% rename from iosApp/flare/UI/Screen/AppearanceLayoutScreen.swift rename to appleApp/ios/UI/Screen/AppearanceLayoutScreen.swift index f1a208ced8..f604079f95 100644 --- a/iosApp/flare/UI/Screen/AppearanceLayoutScreen.swift +++ b/appleApp/ios/UI/Screen/AppearanceLayoutScreen.swift @@ -1,5 +1,7 @@ import SwiftUI +import FlareAppleUI import KotlinSharedUI +import FlareAppleCore struct AppearanceLayoutScreen: View { @StateObject private var statusPresenter = KotlinPresenter(presenter: AppearancePresenter()) diff --git a/iosApp/flare/UI/Screen/AppearanceMediaScreen.swift b/appleApp/ios/UI/Screen/AppearanceMediaScreen.swift similarity index 98% rename from iosApp/flare/UI/Screen/AppearanceMediaScreen.swift rename to appleApp/ios/UI/Screen/AppearanceMediaScreen.swift index d53a2df348..e93a22ea4a 100644 --- a/iosApp/flare/UI/Screen/AppearanceMediaScreen.swift +++ b/appleApp/ios/UI/Screen/AppearanceMediaScreen.swift @@ -1,5 +1,7 @@ import SwiftUI +import FlareAppleUI import KotlinSharedUI +import FlareAppleCore struct AppearanceMediaScreen: View { @StateObject private var statusPresenter = KotlinPresenter(presenter: AppearancePresenter()) diff --git a/iosApp/flare/UI/Screen/AppearanceThemeScreen.swift b/appleApp/ios/UI/Screen/AppearanceThemeScreen.swift similarity index 98% rename from iosApp/flare/UI/Screen/AppearanceThemeScreen.swift rename to appleApp/ios/UI/Screen/AppearanceThemeScreen.swift index 7c9a3dfdf6..15bd89fb22 100644 --- a/iosApp/flare/UI/Screen/AppearanceThemeScreen.swift +++ b/appleApp/ios/UI/Screen/AppearanceThemeScreen.swift @@ -1,5 +1,7 @@ import SwiftUI +import FlareAppleUI import KotlinSharedUI +import FlareAppleCore struct AppearanceThemeScreen: View { @StateObject private var presenter = KotlinPresenter(presenter: SettingsPresenter()) diff --git a/iosApp/flare/UI/Screen/BackportWebLoginScreen.swift b/appleApp/ios/UI/Screen/BackportWebLoginScreen.swift similarity index 100% rename from iosApp/flare/UI/Screen/BackportWebLoginScreen.swift rename to appleApp/ios/UI/Screen/BackportWebLoginScreen.swift diff --git a/iosApp/flare/UI/Screen/BlueskyReportSheet.swift b/appleApp/ios/UI/Screen/BlueskyReportSheet.swift similarity index 95% rename from iosApp/flare/UI/Screen/BlueskyReportSheet.swift rename to appleApp/ios/UI/Screen/BlueskyReportSheet.swift index 25f53b4005..7b69d17a6d 100644 --- a/iosApp/flare/UI/Screen/BlueskyReportSheet.swift +++ b/appleApp/ios/UI/Screen/BlueskyReportSheet.swift @@ -1,5 +1,7 @@ import SwiftUI import KotlinSharedUI +import FlareAppleCore +import AppleFontAwesome struct BlueskyReportSheet: View { @Environment(\.dismiss) private var dismiss @@ -32,7 +34,7 @@ struct BlueskyReportSheet: View { Label { Text("Cancel") } icon: { - Image("fa-xmark") + Image(fontAwesome: .xmark) } } } @@ -50,7 +52,7 @@ struct BlueskyReportSheet: View { Label { Text("Done") } icon: { - Image("fa-check") + Image(fontAwesome: .check) } } .disabled(selecedtReason == nil) diff --git a/iosApp/flare/UI/Screen/ChannelListScreen.swift b/appleApp/ios/UI/Screen/ChannelListScreen.swift similarity index 92% rename from iosApp/flare/UI/Screen/ChannelListScreen.swift rename to appleApp/ios/UI/Screen/ChannelListScreen.swift index a04f7e6d3b..75d91f7a32 100644 --- a/iosApp/flare/UI/Screen/ChannelListScreen.swift +++ b/appleApp/ios/UI/Screen/ChannelListScreen.swift @@ -1,5 +1,8 @@ import SwiftUI +import FlareAppleUI +import AppleFontAwesome @preconcurrency import KotlinSharedUI +import FlareAppleCore struct ChannelListScreen: View { @StateObject private var presenter: KotlinPresenter @@ -27,7 +30,7 @@ struct ChannelListScreen: View { Label { Text("misskey_channel_unfollow") } icon: { - Image(.faMinus) + Image(fontAwesome: .minus) } } } else { @@ -37,7 +40,7 @@ struct ChannelListScreen: View { Label { Text("misskey_channel_follow") } icon: { - Image(.faPlus) + Image(fontAwesome: .plus) } } } @@ -50,7 +53,7 @@ struct ChannelListScreen: View { Label { Text("misskey_channel_unfavorite") } icon: { - Image(.faHeartCircleMinus) + Image(fontAwesome: .heartCircleMinus) } } } else { @@ -60,7 +63,7 @@ struct ChannelListScreen: View { Label { Text("misskey_channel_favorite") } icon: { - Image(.faHeartCirclePlus ) + Image(fontAwesome: .heartCirclePlus) } } } diff --git a/iosApp/flare/UI/Screen/ComposeScreen.swift b/appleApp/ios/UI/Screen/ComposeScreen.swift similarity index 98% rename from iosApp/flare/UI/Screen/ComposeScreen.swift rename to appleApp/ios/UI/Screen/ComposeScreen.swift index b74e6b3b40..6d13b9efb9 100644 --- a/iosApp/flare/UI/Screen/ComposeScreen.swift +++ b/appleApp/ios/UI/Screen/ComposeScreen.swift @@ -1,8 +1,11 @@ import SwiftUI import KotlinSharedUI +import FlareAppleCore +import FlareAppleUI import PhotosUI import SwiftUIIntrospect import SwiftUIBackports +import AppleFontAwesome struct ComposeScreen: View { @Environment(\.dismiss) var dismiss @@ -83,7 +86,7 @@ struct ComposeScreen: View { viewModel.pollViewModel.add() } } label: { - Image("fa-plus") + Image(fontAwesome: .plus) }.disabled(viewModel.pollViewModel.choices.count >= 4) } ForEach($viewModel.pollViewModel.choices) { $choice in @@ -99,7 +102,7 @@ struct ComposeScreen: View { viewModel.pollViewModel.remove(choice: choice) } } label: { - Image("fa-delete-left") + Image(fontAwesome: .deleteLeft) } .disabled(viewModel.pollViewModel.choices.count <= 2) } @@ -153,7 +156,7 @@ struct ComposeScreen: View { }), maxSelectionCount: viewModel.mediaViewModel.maxSize, matching: .any(of: [.images, .videos, .livePhotos])) { - Image("fa-image") + Image(fontAwesome: .image) } } } @@ -166,7 +169,7 @@ struct ComposeScreen: View { viewModel.togglePoll() } }, label: { - Image("fa-square-poll-horizontal") + Image(fontAwesome: .squarePollHorizontal) }) } } @@ -201,7 +204,7 @@ struct ComposeScreen: View { } } }, label: { - Image("fa-circle-exclamation") + Image(fontAwesome: .circleExclamation) }) } } @@ -209,7 +212,7 @@ struct ComposeScreen: View { Button(action: { viewModel.showEmojiPanel() }, label: { - Image("fa-face-smile") + Image(fontAwesome: .faceSmile) }) .popover(isPresented: $viewModel.showEmoji) { NavigationStack { @@ -227,7 +230,7 @@ struct ComposeScreen: View { Label { Text("Cancel") } icon: { - Image("fa-xmark") + Image(fontAwesome: .xmark) } } } @@ -413,7 +416,7 @@ struct ComposeScreen: View { if !selected.isEmpty { accountAvatarStack(users: selected) } - Image("fa-plus") + Image(fontAwesome: .plus) .resizable() .scaledToFit() .frame(width: 14, height: 14) @@ -485,7 +488,7 @@ struct ComposeScreen: View { .frame(width: size, height: size) .clipShape(Circle()) .overlay(Circle().stroke(Color(.systemBackground), lineWidth: 2)) - Image(user.platformIcon.imageName) + Image(fontAwesome: user.platformIcon.fontAwesomeIcon) .resizable() .scaledToFit() .frame(width: size * 0.34, height: size * 0.34) @@ -1011,7 +1014,7 @@ struct ComposeMediaItemView: View { Label { Text("delete") } icon: { - Image("fa-trash") + Image(fontAwesome: .trash) } }) diff --git a/iosApp/flare/UI/Screen/CreateListScreen.swift b/appleApp/ios/UI/Screen/CreateListScreen.swift similarity index 95% rename from iosApp/flare/UI/Screen/CreateListScreen.swift rename to appleApp/ios/UI/Screen/CreateListScreen.swift index 6328714aad..6a83a2ca92 100644 --- a/iosApp/flare/UI/Screen/CreateListScreen.swift +++ b/appleApp/ios/UI/Screen/CreateListScreen.swift @@ -1,6 +1,8 @@ import SwiftUI +import AppleFontAwesome @preconcurrency import KotlinSharedUI import PhotosUI +import FlareAppleCore struct CreateListScreen: View { @Environment(\.dismiss) private var dismiss @@ -27,7 +29,7 @@ struct CreateListScreen: View { .scaledToFit() .frame(width: 96, height: 96) } else { - Image(.faSquareRss) + Image(fontAwesome: .squareRss) .resizable() .scaledToFit() .frame(width: 96, height: 96) @@ -84,7 +86,7 @@ struct CreateListScreen: View { dismiss() } } label: { - Image(.faCheck) + Image(fontAwesome: .check) } .disabled(listName.isEmpty) } @@ -92,7 +94,7 @@ struct CreateListScreen: View { Button { dismiss() } label: { - Image(.faXmark) + Image(fontAwesome: .xmark) } } } diff --git a/iosApp/flare/UI/Screen/DMListScreen.swift b/appleApp/ios/UI/Screen/DMListScreen.swift similarity index 98% rename from iosApp/flare/UI/Screen/DMListScreen.swift rename to appleApp/ios/UI/Screen/DMListScreen.swift index d1ccd73bc1..a4b2a20853 100644 --- a/iosApp/flare/UI/Screen/DMListScreen.swift +++ b/appleApp/ios/UI/Screen/DMListScreen.swift @@ -1,6 +1,9 @@ import SwiftUI +import FlareAppleUI import SwiftUIBackports +import AppleFontAwesome @preconcurrency import KotlinSharedUI +import FlareAppleCore struct DMListScreen: View { let accountType: AccountType @@ -14,7 +17,7 @@ struct DMListScreen: View { AvatarView(data: image.url, customHeader: image.customHeaders) .frame(width: 48, height: 48) } else { - Image("fa-list") + Image(fontAwesome: .list) } VStack( alignment: .leading, diff --git a/appleApp/ios/UI/Screen/DeepLinkAccountPicker.swift b/appleApp/ios/UI/Screen/DeepLinkAccountPicker.swift new file mode 100644 index 0000000000..c86577b8c3 --- /dev/null +++ b/appleApp/ios/UI/Screen/DeepLinkAccountPicker.swift @@ -0,0 +1,20 @@ +import SwiftUI +import FlareAppleUI +import KotlinSharedUI +import SwiftUIBackports + +struct DeepLinkAccountPicker: View { + let originalUrl: String + let data: [MicroBlogKey : Route] + let onNavigate: (Route) -> Void + + var body: some View { + DeepLinkAccountPickerView( + originalUrl: originalUrl, + data: data, + onNavigate: onNavigate + ) + .backport + .navigationSubtitle("deep_link_account_picker_subtitle") + } +} diff --git a/iosApp/flare/UI/Screen/DiscoverScreen.swift b/appleApp/ios/UI/Screen/DiscoverScreen.swift similarity index 98% rename from iosApp/flare/UI/Screen/DiscoverScreen.swift rename to appleApp/ios/UI/Screen/DiscoverScreen.swift index 02b93ba693..7cc020f5c9 100644 --- a/iosApp/flare/UI/Screen/DiscoverScreen.swift +++ b/appleApp/ios/UI/Screen/DiscoverScreen.swift @@ -1,6 +1,9 @@ import SwiftUI +import FlareAppleUI +import AppleFontAwesome @preconcurrency import KotlinSharedUI import Flow +import FlareAppleCore struct DiscoverScreen: View { @Environment(\.openURL) private var openURL @@ -65,7 +68,7 @@ struct DiscoverScreen: View { AvatarView(data: selectedAccount.avatar?.url, customHeader: selectedAccount.avatar?.customHeaders) .frame(width: 24, height: 24) Text(selectedAccount.handle.canonical) - Image("fa-chevron-down") + Image(fontAwesome: .chevronDown) .font(.footnote) .foregroundStyle(.secondary) .scaledToFit() diff --git a/iosApp/flare/UI/Screen/DraftBoxScreen.swift b/appleApp/ios/UI/Screen/DraftBoxScreen.swift similarity index 97% rename from iosApp/flare/UI/Screen/DraftBoxScreen.swift rename to appleApp/ios/UI/Screen/DraftBoxScreen.swift index 3c3f55e338..e32cf29797 100644 --- a/iosApp/flare/UI/Screen/DraftBoxScreen.swift +++ b/appleApp/ios/UI/Screen/DraftBoxScreen.swift @@ -1,5 +1,8 @@ import SwiftUI +import FlareAppleUI import KotlinSharedUI +import FlareAppleCore +import AppleFontAwesome struct DraftBoxScreen: View { let onEditDraft: (String) -> Void @@ -12,7 +15,7 @@ struct DraftBoxScreen: View { Label { Text("No Drafts") } icon: { - Image(.faInbox) + Image(fontAwesome: .inbox) } } } else { @@ -148,7 +151,7 @@ struct DraftItemView: View { Label("Delete", systemImage: "trash") } } label: { - Image(.faEllipsisVertical) + Image(fontAwesome: .ellipsisVertical) .foregroundStyle(.secondary) .frame(width: 28, height: 28) } diff --git a/iosApp/flare/UI/Screen/EditListMemberScreen.swift b/appleApp/ios/UI/Screen/EditListMemberScreen.swift similarity index 89% rename from iosApp/flare/UI/Screen/EditListMemberScreen.swift rename to appleApp/ios/UI/Screen/EditListMemberScreen.swift index 43c98050e2..ace5d4fd21 100644 --- a/iosApp/flare/UI/Screen/EditListMemberScreen.swift +++ b/appleApp/ios/UI/Screen/EditListMemberScreen.swift @@ -1,5 +1,8 @@ import SwiftUI import KotlinSharedUI +import FlareAppleCore +import FlareAppleUI +import AppleFontAwesome struct EditListMemberScreen: View { @Environment(\.dismiss) var dismiss @@ -19,13 +22,13 @@ struct EditListMemberScreen: View { Button(role: .destructive) { presenter.state.removeMember(userKey: data.key) } label: { - Image(.faTrash) + Image(fontAwesome: .trash) } } else { Button { presenter.state.addMember(userKey: data.key) } label: { - Image(.faPlus) + Image(fontAwesome: .plus) } } } onClicked: { @@ -47,7 +50,7 @@ struct EditListMemberScreen: View { Button { dismiss() } label: { - Image("fa-xmark") + Image(fontAwesome: .xmark) } } } diff --git a/iosApp/flare/UI/Screen/EditListScreen.swift b/appleApp/ios/UI/Screen/EditListScreen.swift similarity index 94% rename from iosApp/flare/UI/Screen/EditListScreen.swift rename to appleApp/ios/UI/Screen/EditListScreen.swift index 12a4dd8d7b..7213606d29 100644 --- a/iosApp/flare/UI/Screen/EditListScreen.swift +++ b/appleApp/ios/UI/Screen/EditListScreen.swift @@ -1,6 +1,9 @@ import SwiftUI +import FlareAppleUI +import AppleFontAwesome @preconcurrency import KotlinSharedUI import PhotosUI +import FlareAppleCore struct EditListScreen: View { @Environment(\.dismiss) var dismiss @@ -42,7 +45,7 @@ struct EditListScreen: View { NetworkImage(data: remote) .frame(width: 96, height: 96) } else { - Image(.faSquareRss) + Image(fontAwesome: .squareRss) .resizable() .scaledToFit() .frame(width: 96, height: 96) @@ -94,7 +97,7 @@ struct EditListScreen: View { Label { Text("delete") } icon: { - Image(.faTrash) + Image(fontAwesome: .trash) } } } @@ -105,7 +108,7 @@ struct EditListScreen: View { Label { Text("delete") } icon: { - Image(.faTrash) + Image(fontAwesome: .trash) } } } @@ -135,7 +138,7 @@ struct EditListScreen: View { Button { dismiss() } label: { - Image("fa-xmark") + Image(fontAwesome: .xmark) } } ToolbarItem(placement: .primaryAction) { @@ -145,7 +148,7 @@ struct EditListScreen: View { Label { Text("add") } icon: { - Image(.faPlus) + Image(fontAwesome: .plus) } } } @@ -178,7 +181,7 @@ struct EditListScreen: View { Label { Text("done") } icon: { - Image(.faCheck) + Image(fontAwesome: .check) } } } @@ -186,4 +189,3 @@ struct EditListScreen: View { .disabled(isLoading) } } - diff --git a/iosApp/flare/UI/Screen/EditUserInListScreen.swift b/appleApp/ios/UI/Screen/EditUserInListScreen.swift similarity index 88% rename from iosApp/flare/UI/Screen/EditUserInListScreen.swift rename to appleApp/ios/UI/Screen/EditUserInListScreen.swift index f6d0cacb82..57525b0b4e 100644 --- a/iosApp/flare/UI/Screen/EditUserInListScreen.swift +++ b/appleApp/ios/UI/Screen/EditUserInListScreen.swift @@ -1,5 +1,8 @@ import SwiftUI +import FlareAppleUI import KotlinSharedUI +import FlareAppleCore +import AppleFontAwesome struct EditUserInListScreen: View { @Environment(\.dismiss) private var dismiss @@ -21,13 +24,13 @@ struct EditUserInListScreen: View { Button { presenter.state.removeList(list: item) } label: { - Image(.faTrash) + Image(fontAwesome: .trash) } } else { Button { presenter.state.addList(list: item) } label: { - Image(.faPlus) + Image(fontAwesome: .plus) } } } loadingContent: { @@ -46,7 +49,7 @@ struct EditUserInListScreen: View { Button { dismiss() } label: { - Image(.faXmark) + Image(fontAwesome: .xmark) } } } diff --git a/iosApp/flare/UI/Screen/GalleryDetailScreen.swift b/appleApp/ios/UI/Screen/GalleryDetailScreen.swift similarity index 99% rename from iosApp/flare/UI/Screen/GalleryDetailScreen.swift rename to appleApp/ios/UI/Screen/GalleryDetailScreen.swift index 8f69c6628e..ce4ba10073 100644 --- a/iosApp/flare/UI/Screen/GalleryDetailScreen.swift +++ b/appleApp/ios/UI/Screen/GalleryDetailScreen.swift @@ -1,5 +1,8 @@ import SwiftUI +import FlareAppleUI +import AppleFontAwesome @preconcurrency import KotlinSharedUI +import FlareAppleCore struct GalleryDetailScreen: View { @Environment(\.horizontalSizeClass) private var horizontalSizeClass @@ -58,14 +61,14 @@ struct GalleryDetailScreen: View { ClickContext(launcher: AppleUriLauncher(openUrl: openURL)) ) } label: { - Image("fa-share-nodes") + Image(fontAwesome: .shareNodes) } .disabled(presenter.state.detail.galleryPost?.shareAction == nil) Button { showInfoSheet = true } label: { - Image("fa-chevron-down") + Image(fontAwesome: .chevronDown) } .disabled(presenter.state.detail.galleryPost == nil) } diff --git a/iosApp/flare/UI/Screen/GroupConfigScreen.swift b/appleApp/ios/UI/Screen/GroupConfigScreen.swift similarity index 96% rename from iosApp/flare/UI/Screen/GroupConfigScreen.swift rename to appleApp/ios/UI/Screen/GroupConfigScreen.swift index d146275e16..b54333a3cb 100644 --- a/iosApp/flare/UI/Screen/GroupConfigScreen.swift +++ b/appleApp/ios/UI/Screen/GroupConfigScreen.swift @@ -1,6 +1,9 @@ import SwiftUI +import FlareAppleUI import KotlinSharedUI import SwiftUIBackports +import FlareAppleCore +import AppleFontAwesome struct GroupConfigScreen: View { @Environment(\.dismiss) private var dismiss @@ -73,7 +76,7 @@ struct GroupConfigScreen: View { if tabs.isEmpty { Section { VStack(alignment: .center, spacing: 8) { - Image("fa-table-list") + Image(fontAwesome: .tableList) .resizable() .scaledToFit() .frame(width: 64, height: 64) @@ -97,7 +100,7 @@ struct GroupConfigScreen: View { Button { editItem = tab } label: { - Image("fa-pen") + Image(fontAwesome: .pen) } .buttonStyle(.plain) } @@ -160,14 +163,14 @@ struct GroupConfigScreen: View { Button { dismiss() } label: { - Image(.faXmark) + Image(fontAwesome: .xmark) } } ToolbarItem(placement: .primaryAction) { Button { showAddTabSheet = true } label: { - Image(.faPlus) + Image(fontAwesome: .plus) } } ToolbarItem(placement: .confirmationAction) { @@ -188,7 +191,7 @@ struct GroupConfigScreen: View { ) dismiss() } label: { - Image(.faCheck) + Image(fontAwesome: .check) } .disabled(tabs.isEmpty && item == nil) } diff --git a/iosApp/flare/UI/Screen/HomeTimelineScreen.swift b/appleApp/ios/UI/Screen/HomeTimelineScreen.swift similarity index 61% rename from iosApp/flare/UI/Screen/HomeTimelineScreen.swift rename to appleApp/ios/UI/Screen/HomeTimelineScreen.swift index 138523ffbf..095e454740 100644 --- a/iosApp/flare/UI/Screen/HomeTimelineScreen.swift +++ b/appleApp/ios/UI/Screen/HomeTimelineScreen.swift @@ -1,5 +1,8 @@ import SwiftUI +import FlareAppleUI +import AppleFontAwesome @preconcurrency import KotlinSharedUI +import FlareAppleCore import SwiftUIBackports struct HomeTimelineScreen: View { @@ -13,7 +16,6 @@ struct HomeTimelineScreen: View { @Environment(\.timelineAppearance) private var timelineAppearance @Environment(\.openURL) private var openURL @State private var selectedTabIndex = 0 - @Namespace private var selectedTabIndicatorNamespace @StateObject private var presenter: KotlinPresenter @StateObject private var activeAccountPresenter = KotlinPresenter(presenter: ActiveAccountPresenter()) @StateObject private var loggedInPresenter = KotlinPresenter(presenter: LoggedInPresenter()) @@ -42,7 +44,7 @@ struct HomeTimelineScreen: View { ContentUnavailableView("tab_settings_title", systemImage: "square.grid.2x2") .toolbar { ToolbarItem(placement: .topBarLeading) { - Image(.faGear) + Image(fontAwesome: .gear) .onTapGesture { toSecondaryMenu() } @@ -51,7 +53,7 @@ struct HomeTimelineScreen: View { Button { toTabSetting() } label: { - Image("fa-plus") + Image(fontAwesome: .plus) } } } @@ -70,7 +72,7 @@ struct HomeTimelineScreen: View { Button { toTabSetting() } label: { - Image("fa-sliders") + Image(fontAwesome: .sliders) } composeToolbarButton } @@ -89,93 +91,44 @@ struct HomeTimelineScreen: View { }) .toolbar { leadingToolbarContent - if horizontalSizeClass == .compact { - ToolbarItem(placement: .title) { - Label { - TimelineTabTitle(title: tab.title) - } icon: { - TabIcon(tabItem: tab) - } - .labelStyle(.titleAndIcon) - } - ToolbarTitleMenu { - ForEach(0.. diff --git a/iosApp/flare/UI/Screen/LocalHistoryScreen.swift b/appleApp/ios/UI/Screen/LocalHistoryScreen.swift similarity index 98% rename from iosApp/flare/UI/Screen/LocalHistoryScreen.swift rename to appleApp/ios/UI/Screen/LocalHistoryScreen.swift index c4fb1ef312..4e72137bc8 100644 --- a/iosApp/flare/UI/Screen/LocalHistoryScreen.swift +++ b/appleApp/ios/UI/Screen/LocalHistoryScreen.swift @@ -1,5 +1,7 @@ import SwiftUI +import FlareAppleUI import KotlinSharedUI +import FlareAppleCore struct LocalHistoryScreen: View { @Environment(\.timelineAppearance.aiConfig.agent) private var agentEnabled diff --git a/iosApp/flare/UI/Screen/MediaScreen.swift b/appleApp/ios/UI/Screen/MediaScreen.swift similarity index 93% rename from iosApp/flare/UI/Screen/MediaScreen.swift rename to appleApp/ios/UI/Screen/MediaScreen.swift index 70f5a0eb1b..5d395925c8 100644 --- a/iosApp/flare/UI/Screen/MediaScreen.swift +++ b/appleApp/ios/UI/Screen/MediaScreen.swift @@ -1,6 +1,7 @@ import SwiftUI import KotlinSharedUI import LazyPager +import AppleFontAwesome struct MediaScreen: View { let url: String @@ -41,7 +42,7 @@ struct MediaScreen: View { Button { dismiss() } label: { - Image("fa-xmark") + Image(fontAwesome: .xmark) } } ToolbarItem(placement: .primaryAction) { @@ -58,19 +59,19 @@ struct MediaScreen: View { Button { MediaSaver.shared.saveImage(url: url, customHeaders: customHeaders) } label: { - Image("fa-download") + Image(fontAwesome: .download) } } ToolbarItem(placement: .primaryAction) { if let shareFileURL, shareFileSourceURL == url { ShareLink(item: shareFileURL) { - Image("fa-share-nodes") + Image(fontAwesome: .shareNodes) } .accessibilityLabel("Share image") } else { Button { } label: { - Image("fa-share-nodes") + Image(fontAwesome: .shareNodes) } .disabled(true) .accessibilityLabel("Share image") diff --git a/iosApp/flare/UI/Screen/MisskeyReportSheet.swift b/appleApp/ios/UI/Screen/MisskeyReportSheet.swift similarity index 92% rename from iosApp/flare/UI/Screen/MisskeyReportSheet.swift rename to appleApp/ios/UI/Screen/MisskeyReportSheet.swift index 9550d43474..bd0d5dd387 100644 --- a/iosApp/flare/UI/Screen/MisskeyReportSheet.swift +++ b/appleApp/ios/UI/Screen/MisskeyReportSheet.swift @@ -1,5 +1,7 @@ import SwiftUI import KotlinSharedUI +import FlareAppleCore +import AppleFontAwesome struct MisskeyReportSheet: View { @Environment(\.dismiss) private var dismiss @@ -27,7 +29,7 @@ struct MisskeyReportSheet: View { Label { Text("Cancel") } icon: { - Image("fa-xmark") + Image(fontAwesome: .xmark) } } } @@ -41,7 +43,7 @@ struct MisskeyReportSheet: View { Label { Text("Done") } icon: { - Image("fa-check") + Image(fontAwesome: .check) } } .disabled(reason.isEmpty) diff --git a/iosApp/flare/UI/Screen/NostrRelaysScreen.swift b/appleApp/ios/UI/Screen/NostrRelaysScreen.swift similarity index 92% rename from iosApp/flare/UI/Screen/NostrRelaysScreen.swift rename to appleApp/ios/UI/Screen/NostrRelaysScreen.swift index 77660a61d9..f418f9ca20 100644 --- a/iosApp/flare/UI/Screen/NostrRelaysScreen.swift +++ b/appleApp/ios/UI/Screen/NostrRelaysScreen.swift @@ -1,5 +1,7 @@ import SwiftUI +import AppleFontAwesome @preconcurrency import KotlinSharedUI +import FlareAppleCore struct NostrRelaysScreen: View { let accountKey: MicroBlogKey @@ -24,7 +26,7 @@ struct NostrRelaysScreen: View { Label { Text("Delete") } icon: { - Image("fa-trash") + Image(fontAwesome: .trash) } } } @@ -35,7 +37,7 @@ struct NostrRelaysScreen: View { Label { Text("Delete") } icon: { - Image("fa-trash") + Image(fontAwesome: .trash) } } } @@ -54,7 +56,7 @@ struct NostrRelaysScreen: View { Button { showAddAlert = true } label: { - Image("fa-plus") + Image(fontAwesome: .plus) } } } diff --git a/iosApp/flare/UI/Screen/NotificationScreen.swift b/appleApp/ios/UI/Screen/NotificationScreen.swift similarity index 98% rename from iosApp/flare/UI/Screen/NotificationScreen.swift rename to appleApp/ios/UI/Screen/NotificationScreen.swift index c54048d932..8e2de6d4b6 100644 --- a/iosApp/flare/UI/Screen/NotificationScreen.swift +++ b/appleApp/ios/UI/Screen/NotificationScreen.swift @@ -1,6 +1,9 @@ import SwiftUI +import FlareAppleUI import SwiftUIBackports +import AppleFontAwesome @preconcurrency import KotlinSharedUI +import FlareAppleCore struct NotificationScreen: View { @Environment(\.horizontalSizeClass) private var horizontalSizeClass @@ -288,7 +291,7 @@ struct NotificationAccountsMenu: View { .lineLimit(1) } } - Image("fa-chevron-down") + Image(fontAwesome: .chevronDown) .font(.footnote) .foregroundStyle(.secondary) .scaledToFit() diff --git a/iosApp/flare/UI/Screen/PostActionLayoutScreen.swift b/appleApp/ios/UI/Screen/PostActionLayoutScreen.swift similarity index 91% rename from iosApp/flare/UI/Screen/PostActionLayoutScreen.swift rename to appleApp/ios/UI/Screen/PostActionLayoutScreen.swift index f233243623..e9c6c8c71f 100644 --- a/iosApp/flare/UI/Screen/PostActionLayoutScreen.swift +++ b/appleApp/ios/UI/Screen/PostActionLayoutScreen.swift @@ -1,5 +1,8 @@ import SwiftUI +import FlareAppleUI import KotlinSharedUI +import FlareAppleCore +import AppleFontAwesome struct PostActionLayoutScreen: View { @StateObject private var statusPresenter = KotlinPresenter(presenter: AppearancePresenter()) @@ -71,7 +74,7 @@ struct PostActionLayoutScreen: View { totalCount: Int ) -> some View { HStack(spacing: 12) { - Image(family.imageName) + Image(fontAwesome: family.fontAwesomeIcon) .frame(width: 24, height: 24) .foregroundStyle(.primary) Text(family.title) @@ -114,7 +117,7 @@ struct PostActionLayoutScreen: View { } .disabled(index >= totalCount - 1) } label: { - Image("fa-ellipsis-vertical") + Image(fontAwesome: .ellipsisVertical) .frame(width: 32, height: 32) } .buttonStyle(.borderless) @@ -197,23 +200,23 @@ private extension PostActionFamily { } } - var imageName: String { + var fontAwesomeIcon: FontAwesomeIcon { switch self { - case .reply: return "fa-reply" - case .comment: return "fa-comment-dots" - case .repost: return "fa-retweet" - case .quote: return "fa-reply" - case .like: return "fa-heart" - case .react: return "fa-plus" - case .translate: return "fa-language" - case .bookmark: return "fa-bookmark" - case .favorite: return "fa-star" - case .share: return "fa-share-nodes" - case .fxShare: return "fa-share-nodes" - case .delete: return "fa-trash" - case .report: return "fa-circle-info" - case .muteUser: return "fa-volume-xmark" - case .blockUser: return "fa-user-slash" + case .reply: return .reply + case .comment: return .commentDots + case .repost: return .retweet + case .quote: return .reply + case .like: return .heart + case .react: return .plus + case .translate: return .language + case .bookmark: return .bookmark + case .favorite: return .star + case .share: return .shareNodes + case .fxShare: return .shareNodes + case .delete: return .trash + case .report: return .circleInfo + case .muteUser: return .volumeXmark + case .blockUser: return .userSlash } } } diff --git a/iosApp/flare/UI/Screen/ProfileInsightSheet.swift b/appleApp/ios/UI/Screen/ProfileInsightSheet.swift similarity index 96% rename from iosApp/flare/UI/Screen/ProfileInsightSheet.swift rename to appleApp/ios/UI/Screen/ProfileInsightSheet.swift index 03b6cdeb87..d5f625fbfd 100644 --- a/iosApp/flare/UI/Screen/ProfileInsightSheet.swift +++ b/appleApp/ios/UI/Screen/ProfileInsightSheet.swift @@ -1,5 +1,8 @@ import SwiftUI import KotlinSharedUI +import FlareAppleCore +import FlareAppleUI +import AppleFontAwesome struct ProfileInsightSheet: View { @Environment(\.dismiss) private var dismiss @@ -51,7 +54,7 @@ struct ProfileInsightSheet: View { Label { Text("Cancel") } icon: { - Image("fa-xmark") + Image(fontAwesome: .xmark) } } } diff --git a/iosApp/flare/UI/Screen/ProfileScreen.swift b/appleApp/ios/UI/Screen/ProfileScreen.swift similarity index 90% rename from iosApp/flare/UI/Screen/ProfileScreen.swift rename to appleApp/ios/UI/Screen/ProfileScreen.swift index 43df900f59..324816cace 100644 --- a/iosApp/flare/UI/Screen/ProfileScreen.swift +++ b/appleApp/ios/UI/Screen/ProfileScreen.swift @@ -1,6 +1,9 @@ import SwiftUI +import FlareAppleUI import SwiftUIBackports +import AppleFontAwesome @preconcurrency import KotlinSharedUI +import FlareAppleCore import Combine struct ProfileScreen: View { @@ -83,7 +86,7 @@ struct ProfileScreen: View { Button { onProfileInsight(userState.data.key) } label: { - Image("fa-robot") + Image(fontAwesome: .robot) } .accessibilityLabel(Text(String(localized: "profile_insight_title", defaultValue: "Profile insight"))) } @@ -238,22 +241,6 @@ struct ProfileScreen: View { } } -private struct ProfileTabPicker: View { - let tabs: [ProfileState.Tab] - @Binding var selectedTab: Int - - var body: some View { - Picker(selection: $selectedTab) { - ForEach(0.. Void @@ -275,28 +262,6 @@ private struct BlockedProfileGate: View { } } -private func profileTabTitle(for tab: ProfileState.Tab) -> String { - tab.name.text -} - -private func profileTimelineID(for tab: ProfileState.Tab) -> String { - switch onEnum(of: tab) { - case .timeline: - "Timeline_\(tab.name.name)" - case .media: - "Media_\(tab.name.name)" - } -} - -private func profileTimelinePresenter(for tab: ProfileState.Tab) -> TimelinePresenter { - switch onEnum(of: tab) { - case .timeline(let tab): - tab.presenter - case .media(let tab): - tab.presenter.getMediaTimelinePresenter() - } -} - private struct ProfileCompatTimelineView: UIViewControllerRepresentable { let profileState: ProfileState let tabs: [ProfileState.Tab] @@ -733,68 +698,6 @@ extension ProfileScreen { } } -struct ProfileHeader: View { - let user: UiState - let relation: UiState - let followButtonState: UiState - let isMe: UiState - let onFollowClick: (UiProfile, FollowButtonState) -> Void - let onFollowingClick: (MicroBlogKey) -> Void - let onFansClick: (MicroBlogKey) -> Void - var body: some View { - switch onEnum(of: user) { - case .error: - Text("error") - case .loading: - CommonProfileHeader( - user: createSampleUser(), - relation: relation, - followButtonState: followButtonState, - isMe: isMe, - onFollowClick: { _ in }, - onFollowingClick: {}, - onFansClick: {} - ) - .redacted(reason: .placeholder) - case .success(let data): - ProfileHeaderSuccess( - user: data.data, - relation: relation, - followButtonState: followButtonState, - isMe: isMe, - onFollowClick: { followButtonState in onFollowClick(data.data, followButtonState) }, - onFollowingClick: onFollowingClick, - onFansClick: onFansClick - ) - } - } -} - -struct ProfileHeaderSuccess: View { - let user: UiProfile - let relation: UiState - let followButtonState: UiState - let isMe: UiState - let onFollowClick: (FollowButtonState) -> Void - let onFollowingClick: (MicroBlogKey) -> Void - let onFansClick: (MicroBlogKey) -> Void - var body: some View { - CommonProfileHeader( - user: user, - relation: relation, - followButtonState: followButtonState, - isMe: isMe, - onFollowClick: onFollowClick, - onFollowingClick: { - onFollowingClick(user.key) - }, - onFansClick: { - onFansClick(user.key) - } - ) - } -} - struct ProfileWithUserNameAndHostScreen: View { @StateObject private var presenter: KotlinPresenter let accountType: AccountType diff --git a/iosApp/flare/UI/Screen/RssDetailScreen.swift b/appleApp/ios/UI/Screen/RssDetailScreen.swift similarity index 99% rename from iosApp/flare/UI/Screen/RssDetailScreen.swift rename to appleApp/ios/UI/Screen/RssDetailScreen.swift index 004ed5b430..253c600ed1 100644 --- a/iosApp/flare/UI/Screen/RssDetailScreen.swift +++ b/appleApp/ios/UI/Screen/RssDetailScreen.swift @@ -1,8 +1,11 @@ import SwiftUI +import FlareAppleUI import KotlinSharedUI import SafariServices import SwiftUIBackports import WebKit +import FlareAppleCore +import AppleFontAwesome struct RssDetailScreen: View { @State private var webViewHeight: CGFloat = .zero @@ -233,7 +236,7 @@ private struct RssArticleContentView: View { message: Text(document.title), preview: SharePreview(document.title) ) { - Image(.faShareNodes) + Image(fontAwesome: .shareNodes) } } } @@ -242,7 +245,7 @@ private struct RssArticleContentView: View { Button { showTranslate = true } label: { - Image(.faLanguage) + Image(fontAwesome: .language) } } } diff --git a/iosApp/flare/UI/Screen/RssScreen.swift b/appleApp/ios/UI/Screen/RssScreen.swift similarity index 94% rename from iosApp/flare/UI/Screen/RssScreen.swift rename to appleApp/ios/UI/Screen/RssScreen.swift index 22f85270e0..eb5f7e09d8 100644 --- a/iosApp/flare/UI/Screen/RssScreen.swift +++ b/appleApp/ios/UI/Screen/RssScreen.swift @@ -1,6 +1,9 @@ import SwiftUI +import FlareAppleUI import KotlinSharedUI import UniformTypeIdentifiers +import FlareAppleCore +import AppleFontAwesome struct RssScreen: View { @StateObject private var presenter = KotlinPresenter(presenter: RssListWithTabsPresenter()) @@ -20,7 +23,7 @@ struct RssScreen: View { Button { selectedEditItem = item } label: { - Image("fa-pen") + Image(fontAwesome: .pen) } .buttonStyle(.plain) } @@ -32,7 +35,7 @@ struct RssScreen: View { Label { Text("delete") } icon: { - Image("fa-trash") + Image(fontAwesome: .trash) } } @@ -48,7 +51,7 @@ struct RssScreen: View { exportedOPMLContent = try? await ExportOPMLPresenter().export() } } label: { - Image("fa-file-export") + Image(fontAwesome: .fileExport) } .fileExporter( isPresented: Binding( @@ -72,7 +75,7 @@ struct RssScreen: View { Button { showAddSheet = true } label: { - Image("fa-plus") + Image(fontAwesome: .plus) } } } @@ -126,16 +129,16 @@ struct EditRssSheet: View { StateView(state: presenter.state.checkState) { state in switch onEnum(of: state) { case .rssFeed: - Image("fa-circle-check").foregroundColor(.green) + Image(fontAwesome: .circleCheck).foregroundColor(.green) case .rssHub: - Image("fa-circle-chevron-down").foregroundColor(.secondary) + Image(fontAwesome: .circleChevronDown).foregroundColor(.secondary) case .rssSources: - Image("fa-circle-chevron-down").foregroundColor(.secondary) + Image(fontAwesome: .circleChevronDown).foregroundColor(.secondary) case .subscriptionInstance: - Image("fa-circle-check").foregroundColor(.green) + Image(fontAwesome: .circleCheck).foregroundColor(.green) } } errorContent: { _ in - Image("fa-circle-exclamation").foregroundColor(.red) + Image(fontAwesome: .circleExclamation).foregroundColor(.red) } loadingContent: { ProgressView().frame(width: 20, height: 20) } @@ -182,7 +185,7 @@ struct EditRssSheet: View { NetworkImage(data: favIcon) .frame(width: 24, height: 24) } else { - Image("fa-square-rss") + Image(fontAwesome: .squareRss) } } Text(rssFeed.url) @@ -230,9 +233,9 @@ struct EditRssSheet: View { StateView(state: presenter.state.inputState) { inputState in if case .rssHub(let rssHub) = onEnum(of: inputState) { StateView(state: rssHub.checkState) { _ in - Image("fa-circle-check").foregroundColor(.green) + Image(fontAwesome: .circleCheck).foregroundColor(.green) } errorContent : { _ in - Image("fa-circle-exclamation").foregroundColor(.red) + Image(fontAwesome: .circleExclamation).foregroundColor(.red) } loadingContent: { ProgressView().frame(width: 20, height: 20) } @@ -340,7 +343,7 @@ struct EditRssSheet: View { Label { Text("Cancel") } icon: { - Image("fa-xmark") + Image(fontAwesome: .xmark) } } } @@ -370,7 +373,7 @@ struct EditRssSheet: View { Label { Text("Done") } icon: { - Image("fa-check") + Image(fontAwesome: .check) } } } diff --git a/iosApp/flare/UI/Screen/SearchScreen.swift b/appleApp/ios/UI/Screen/SearchScreen.swift similarity index 98% rename from iosApp/flare/UI/Screen/SearchScreen.swift rename to appleApp/ios/UI/Screen/SearchScreen.swift index 445edd76b7..1133083811 100644 --- a/iosApp/flare/UI/Screen/SearchScreen.swift +++ b/appleApp/ios/UI/Screen/SearchScreen.swift @@ -1,5 +1,8 @@ import SwiftUI +import FlareAppleUI +import AppleFontAwesome @preconcurrency import KotlinSharedUI +import FlareAppleCore struct SearchScreen: View { @Environment(\.openURL) private var openURL @@ -104,7 +107,7 @@ struct SearchScreen: View { AvatarView(data: selectedAccount.avatar?.url, customHeader: selectedAccount.avatar?.customHeaders) .frame(width: 24, height: 24) Text(selectedAccount.handle.canonical) - Image("fa-chevron-down") + Image(fontAwesome: .chevronDown) .font(.footnote) .foregroundStyle(.secondary) .scaledToFit() @@ -230,7 +233,7 @@ struct AskAiSearchAccessory: View { Label { Text("ask_ai") } icon: { - Image("fa-robot") + Image(fontAwesome: .robot) } .frame(maxWidth: .infinity) } diff --git a/iosApp/flare/UI/Screen/SecondaryTabsScreen.swift b/appleApp/ios/UI/Screen/SecondaryTabsScreen.swift similarity index 88% rename from iosApp/flare/UI/Screen/SecondaryTabsScreen.swift rename to appleApp/ios/UI/Screen/SecondaryTabsScreen.swift index 00feda29ee..88cb0abf9f 100644 --- a/iosApp/flare/UI/Screen/SecondaryTabsScreen.swift +++ b/appleApp/ios/UI/Screen/SecondaryTabsScreen.swift @@ -1,5 +1,8 @@ import SwiftUI import KotlinSharedUI +import FlareAppleCore +import FlareAppleUI +import AppleFontAwesome struct SecondaryTabsScreen: View { @Environment(\.dismiss) private var dismiss @@ -23,7 +26,7 @@ struct SecondaryTabsScreen: View { Label { Text(tab.title.text) } icon: { - Image(tab.icon.imageName) + Image(fontAwesome: tab.icon.fontAwesomeIcon) } } .buttonStyle(.plain) @@ -46,21 +49,21 @@ struct SecondaryTabsScreen: View { Label { Text("Drafts") } icon: { - Image(.faPenToSquare) + Image(fontAwesome: .penToSquare) } } NavigationLink(value: Route.rssManagement) { Label { Text("settings_rss_management_title") } icon: { - Image("fa-square-rss") + Image(fontAwesome: .squareRss) } } NavigationLink(value: Route.localHostory) { Label { Text("local_history_title") } icon: { - Image("fa-clock-rotate-left") + Image(fontAwesome: .clockRotateLeft) } } if aiAgentEnabledPresenter.state.enabled { @@ -68,7 +71,7 @@ struct SecondaryTabsScreen: View { Label { Text("settings_agent_history_title") } icon: { - Image("fa-robot") + Image(fontAwesome: .robot) } } } @@ -76,7 +79,7 @@ struct SecondaryTabsScreen: View { Label { Text("settings_title") } icon: { - Image("fa-gear") + Image(fontAwesome: .gear) } } } @@ -87,7 +90,7 @@ struct SecondaryTabsScreen: View { Button { dismiss() } label: { - Image("fa-xmark") + Image(fontAwesome: .xmark) } } } diff --git a/iosApp/flare/UI/Screen/ServiceSelectionScreen.swift b/appleApp/ios/UI/Screen/ServiceSelectionScreen.swift similarity index 93% rename from iosApp/flare/UI/Screen/ServiceSelectionScreen.swift rename to appleApp/ios/UI/Screen/ServiceSelectionScreen.swift index 6e4526c17e..8ce3789478 100644 --- a/iosApp/flare/UI/Screen/ServiceSelectionScreen.swift +++ b/appleApp/ios/UI/Screen/ServiceSelectionScreen.swift @@ -1,9 +1,11 @@ import SwiftUI +import FlareAppleUI +import AppleFontAwesome @preconcurrency import KotlinSharedUI import AuthenticationServices import WebKit -import CoreImage.CIFilterBuiltins import Combine +import FlareAppleCore private enum ServiceSelectionAnimation { static let standard: Animation = .easeInOut(duration: 0.22) @@ -19,7 +21,6 @@ private enum ServiceSelectionAnimation { struct ServiceSelectionScreen: View { let toHome: () -> Void - @StateObject private var presenter: KotlinPresenter @State private var instanceInput = "" @State private var selectedMethods: [String: LoginMethodType] = [:] @@ -99,7 +100,7 @@ struct ServiceSelectionScreen: View { state.setFilter(value: "") selectedMethods.removeAll() } label: { - Image(instanceInput.isEmpty ? "fa-magnifying-glass" : "fa-xmark") + Image(fontAwesome: instanceInput.isEmpty ? .magnifyingGlass : .xmark) .resizable() .scaledToFit() .frame(width: 16, height: 16) @@ -117,7 +118,7 @@ struct ServiceSelectionScreen: View { ZStack { switch onEnum(of: state.detectedPlatformType) { case .success(let success): - Image(state.platformIcon(platformType: success.data.platformType).imageName) + Image(fontAwesome: state.platformIcon(platformType: success.data.platformType).fontAwesomeIcon) .resizable() .scaledToFit() .transition(ServiceSelectionAnimation.inline) @@ -189,7 +190,7 @@ struct ServiceSelectionScreen: View { private func platformHeader(state: ServiceSelectState, node: NodeData) -> some View { HStack(spacing: 8) { - Image(state.platformIcon(platformType: node.platformType).imageName) + Image(fontAwesome: state.platformIcon(platformType: node.platformType).fontAwesomeIcon) .resizable() .scaledToFit() .frame(width: 24, height: 24) @@ -210,7 +211,7 @@ struct ServiceSelectionScreen: View { successContent: { instance in ServiceInstanceRow( instance: instance, - iconName: state.platformIcon(platformType: instance.type).imageName + icon: state.platformIcon(platformType: instance.type).fontAwesomeIcon ) { select(instance: instance, state: state) } @@ -511,7 +512,7 @@ private struct QRLoginView: View { private struct ServiceInstanceRow: View { let instance: UiInstance - let iconName: String + let icon: FontAwesomeIcon let onSelect: () -> Void var body: some View { @@ -541,7 +542,7 @@ private struct ServiceInstanceRow: View { .frame(maxWidth: 32) .clipShape(RoundedRectangle(cornerRadius: 4)) } else { - Image(iconName) + Image(fontAwesome: icon) .resizable() .scaledToFit() } @@ -602,41 +603,6 @@ private struct LoginAgreementView: View { } } -private struct QRCodeView: View { - let text: String - - var body: some View { - if let image = makeQRCode(from: text) { - Image(uiImage: image) - .interpolation(.none) - .resizable() - .scaledToFit() - .padding(12) - } else { - Image(systemName: "qrcode") - .resizable() - .scaledToFit() - .padding(40) - .foregroundStyle(.secondary) - } - } - - private func makeQRCode(from text: String) -> UIImage? { - let filter = CIFilter.qrCodeGenerator() - filter.message = Data(text.utf8) - - guard let outputImage = filter.outputImage else { - return nil - } - - let scaledImage = outputImage.transformed(by: CGAffineTransform(scaleX: 10, y: 10)) - guard let cgImage = CIContext().createCGImage(scaledImage, from: scaledImage.extent) else { - return nil - } - return UIImage(cgImage: cgImage) - } -} - private enum ServiceSelectCopy { static let welcomeTitle = String(localized: "service_select_welcome_title", defaultValue: "Welcome to Flare") static let welcomeMessage = String(localized: "service_select_welcome_message", defaultValue: "Enter a server to get started.") diff --git a/iosApp/flare/UI/Screen/SettingsScreen.swift b/appleApp/ios/UI/Screen/SettingsScreen.swift similarity index 84% rename from iosApp/flare/UI/Screen/SettingsScreen.swift rename to appleApp/ios/UI/Screen/SettingsScreen.swift index 4ce7d56e13..a74bd24f70 100644 --- a/iosApp/flare/UI/Screen/SettingsScreen.swift +++ b/appleApp/ios/UI/Screen/SettingsScreen.swift @@ -1,5 +1,8 @@ import SwiftUI +import FlareAppleUI import KotlinSharedUI +import FlareAppleCore +import AppleFontAwesome struct SettingsScreen: View { var body: some View { @@ -9,7 +12,7 @@ struct SettingsScreen: View { Text("account_management_title") Text("account_management_description") } icon: { - Image(.faCircleUser) + Image(fontAwesome: .circleUser) } } @@ -19,7 +22,7 @@ struct SettingsScreen: View { Text("appearance_theme_group_title") Text("appearance_theme_group_subtitle") } icon: { - Image("fa-palette") + Image(fontAwesome: .palette) } } NavigationLink(value: Route.appearanceLayout) { @@ -27,7 +30,7 @@ struct SettingsScreen: View { Text("appearance_layout_group_title") Text("appearance_layout_group_subtitle") } icon: { - Image("fa-table-list") + Image(fontAwesome: .tableList) } } NavigationLink(value: Route.appearanceDisplay) { @@ -35,7 +38,7 @@ struct SettingsScreen: View { Text("appearance_display_group_title") Text("appearance_display_group_subtitle") } icon: { - Image(.faNewspaper) + Image(fontAwesome: .newspaper) } } NavigationLink(value: Route.appearanceMedia) { @@ -43,7 +46,7 @@ struct SettingsScreen: View { Text("appearance_media_group_title") Text("appearance_media_group_subtitle") } icon: { - Image(.faPhotoFilm) + Image(fontAwesome: .photoFilm) } } NavigationLink(value: Route.appIconSettings) { @@ -51,7 +54,7 @@ struct SettingsScreen: View { Text("App Icon") Text("Choose the icon shown on your Home Screen") } icon: { - Image("fa-palette") + Image(fontAwesome: .palette) } } if let url = URL(string: UIApplication.openSettingsURLString) { @@ -62,7 +65,7 @@ struct SettingsScreen: View { Text("system_settings_title") Text("system_settings_description") } icon: { - Image(.faGear) + Image(fontAwesome: .gear) } } } @@ -72,7 +75,7 @@ struct SettingsScreen: View { // Label { // Text("more_panel_customize") // } icon: { -// Image("fa-table-list") +// Image(fontAwesome: .tableList) // } // } // } @@ -84,7 +87,7 @@ struct SettingsScreen: View { Text("local_filter_title") Text("local_filter_description") } icon: { - Image("fa-filter") + Image(fontAwesome: .filter) } } NavigationLink(value: Route.storage) { @@ -92,7 +95,7 @@ struct SettingsScreen: View { Text("storage_title") Text("storage_description") } icon: { - Image("fa-database") + Image(fontAwesome: .database) } } } @@ -103,7 +106,7 @@ struct SettingsScreen: View { Text("ai_config_title") Text("ai_config_description") } icon: { - Image("fa-robot") + Image(fontAwesome: .robot) } } @@ -112,7 +115,7 @@ struct SettingsScreen: View { Text("settings_translation_title") Text("settings_translation_description") } icon: { - Image("fa-language") + Image(fontAwesome: .language) } } } @@ -123,7 +126,7 @@ struct SettingsScreen: View { Text("about_title") Text("about_description") } icon: { - Image("fa-circle-info") + Image(fontAwesome: .circleInfo) } } } diff --git a/iosApp/flare/UI/Screen/SplashScreen.swift b/appleApp/ios/UI/Screen/SplashScreen.swift similarity index 100% rename from iosApp/flare/UI/Screen/SplashScreen.swift rename to appleApp/ios/UI/Screen/SplashScreen.swift diff --git a/iosApp/flare/UI/Screen/StatusAddReactionSheet.swift b/appleApp/ios/UI/Screen/StatusAddReactionSheet.swift similarity index 91% rename from iosApp/flare/UI/Screen/StatusAddReactionSheet.swift rename to appleApp/ios/UI/Screen/StatusAddReactionSheet.swift index 356b3f4501..b5bc94fe23 100644 --- a/iosApp/flare/UI/Screen/StatusAddReactionSheet.swift +++ b/appleApp/ios/UI/Screen/StatusAddReactionSheet.swift @@ -1,5 +1,8 @@ import SwiftUI import KotlinSharedUI +import FlareAppleCore +import FlareAppleUI +import AppleFontAwesome struct StatusAddReactionSheet: View { let accountType: AccountType @@ -24,7 +27,7 @@ struct StatusAddReactionSheet: View { Label { Text("Cancel") } icon: { - Image("fa-xmark") + Image(fontAwesome: .xmark) } } } diff --git a/iosApp/flare/UI/Screen/StatusDetailScreen.swift b/appleApp/ios/UI/Screen/StatusDetailScreen.swift similarity index 96% rename from iosApp/flare/UI/Screen/StatusDetailScreen.swift rename to appleApp/ios/UI/Screen/StatusDetailScreen.swift index 90bbd5d7f9..7f6504c6aa 100644 --- a/iosApp/flare/UI/Screen/StatusDetailScreen.swift +++ b/appleApp/ios/UI/Screen/StatusDetailScreen.swift @@ -1,5 +1,7 @@ import SwiftUI +import FlareAppleUI @preconcurrency import KotlinSharedUI +import FlareAppleCore struct StatusDetailScreen: View { @Environment(\.timelineAppearance.timelineDisplayMode) private var timelineDisplayMode diff --git a/iosApp/flare/UI/Screen/StatusInsightSheet.swift b/appleApp/ios/UI/Screen/StatusInsightSheet.swift similarity index 97% rename from iosApp/flare/UI/Screen/StatusInsightSheet.swift rename to appleApp/ios/UI/Screen/StatusInsightSheet.swift index bb8370077b..f2e3e3c745 100644 --- a/iosApp/flare/UI/Screen/StatusInsightSheet.swift +++ b/appleApp/ios/UI/Screen/StatusInsightSheet.swift @@ -1,5 +1,8 @@ import SwiftUI +import FlareAppleUI import KotlinSharedUI +import FlareAppleCore +import AppleFontAwesome struct StatusInsightSheet: View { @Environment(\.dismiss) private var dismiss @@ -54,7 +57,7 @@ struct StatusInsightSheet: View { Label { Text("Cancel") } icon: { - Image("fa-xmark") + Image(fontAwesome: .xmark) } } } @@ -128,7 +131,7 @@ struct StatusInsightCurrentTrace: View { var body: some View { HStack(spacing: 8) { - Image("fa-robot") + Image(fontAwesome: .robot) Text(verbatim: trace) .font(.body) .shimmeringText() diff --git a/iosApp/flare/UI/Screen/StatusMediaScreen.swift b/appleApp/ios/UI/Screen/StatusMediaScreen.swift similarity index 63% rename from iosApp/flare/UI/Screen/StatusMediaScreen.swift rename to appleApp/ios/UI/Screen/StatusMediaScreen.swift index 8539d48b1c..89ba121625 100644 --- a/iosApp/flare/UI/Screen/StatusMediaScreen.swift +++ b/appleApp/ios/UI/Screen/StatusMediaScreen.swift @@ -1,13 +1,15 @@ import SwiftUI +import FlareAppleUI import KotlinSharedUI import LazyPager import AVKit import Photos import Kingfisher import SwiftUIBackports -import VideoPlayer import Combine import UIKit +import FlareAppleCore +import AppleFontAwesome struct StatusMediaScreen: View { @Environment(\.dismiss) var dismiss @@ -152,7 +154,7 @@ struct StatusMediaScreen: View { Button { dismiss() } label: { - Image("fa-xmark") + Image(fontAwesome: .xmark) } } if !medias.isEmpty { @@ -171,19 +173,19 @@ struct StatusMediaScreen: View { Button { MediaSaver.shared.saveImage(url: selectedMedia.url, customHeaders: selectedMedia.customHeaders) } label: { - Image("fa-download") + Image(fontAwesome: .download) } } ToolbarItem(placement: .primaryAction) { if let shareFileURL, shareFileSourceURL == selectedMedia.url { ShareLink(item: shareFileURL) { - Image("fa-share-nodes") + Image(fontAwesome: .shareNodes) } .accessibilityLabel("Share image") } else { Button { } label: { - Image("fa-share-nodes") + Image(fontAwesome: .shareNodes) } .disabled(true) .accessibilityLabel("Share image") @@ -194,7 +196,7 @@ struct StatusMediaScreen: View { Button { MediaSaver.shared.saveVideo(url: video.url, customHeaders: video.customHeaders) } label: { - Image("fa-download") + Image(fontAwesome: .download) } } } @@ -408,7 +410,7 @@ struct VideoControlView: View { Button { isPlaying.toggle() } label: { - Image(isPlaying ? "fa-pause" : "fa-play") + Image(fontAwesome: isPlaying ? .pause : .play) .font(.title2) .frame(height: 24) .contentTransition(.symbolEffect(.replace)) @@ -513,366 +515,6 @@ extension StatusMediaScreen { } } -enum VideoState { - case idle - case loading - case playing(Double) - case paused(Double) - case error(Error) -} - -struct StatusMediaVideoView: View { - @Binding var play: Bool - @Binding var videoState: VideoState - @Binding var time: CMTime - @Binding var playbackRate: Float - @State private var isAppeared: Bool = false - @State private var playbackResumeTask: Task? - @State private var fastPlaybackWasPlaying: Bool = false - @State private var isFastPlaybackActive: Bool = false - @State private var seekFeedback: SeekFeedback? - @State private var seekFeedbackOpacity: Double = 0 - @State private var seekFeedbackTask: Task? - let data: UiMediaVideo - private let seekInterval: Double = 5 - private let normalPlaybackRate: Float = 1 - private let fastPlaybackRate: Float = 2 - - init( - data: UiMediaVideo, - play: Binding, - videoState: Binding, - time: Binding, - playbackRate: Binding - ) { - self.data = data - self._play = play - self._videoState = videoState - self._time = time - self._playbackRate = playbackRate - } - - var body: some View { - Color.clear -// .opacity(0.2) - .overlay { - if case .idle = videoState { - NetworkImage(data: data.thumbnailUrl, customHeader: data.customHeaders) - .scaledToFit() - .allowsHitTesting(false) - } else { - EmptyView() - } - } - .clipped() - .overlay { - if let videoURL = URL(string: data.url) { - VideoPlayer(url: videoURL, play: $play, time: $time) - .mute(false) - .autoReplay(true) - .speedRate(playbackRate) - .onStateChanged { state in - switch state { - case .playing(let duration): videoState = .playing(duration) - case .loading: videoState = .loading - case .paused: - if case .playing(let duration) = videoState { - videoState = .paused(duration) - } else if case .paused(let duration) = videoState { - videoState = .paused(duration) - } else { - videoState = .idle - } - case .error(let error): videoState = .error(error) - } - } - .contentMode(.scaleAspectFit) - .allowsHitTesting(false) - } - } - .overlay { - VideoGestureOverlay( - onDoubleTap: { x, width in - if x < width / 2 { - seek(by: -seekInterval) - showSeekFeedback(.backward) - } else { - seek(by: seekInterval) - showSeekFeedback(.forward) - } - }, - onLongPressChanged: { pressing in - if pressing { - beginFastPlayback() - } else { - endFastPlayback() - } - } - ) - .frame(maxWidth: .infinity, maxHeight: .infinity) - .accessibilityHidden(true) - } - .overlay { - if let seekFeedback { - HStack { - if seekFeedback == .forward { - Spacer() - } - Image(systemName: seekFeedback.iconName) - .font(.system(size: 44, weight: .semibold)) - .foregroundStyle(.white) - .padding(20) - .background(.black.opacity(0.55), in: .circle) - if seekFeedback == .backward { - Spacer() - } - } - .padding(.horizontal, 56) - .opacity(seekFeedbackOpacity) - } - } - .onDisappear { - endFastPlayback() - seekFeedbackTask?.cancel() - } - } - - private func seek(by offset: Double) { - let currentSeconds = time.seconds.isFinite ? time.seconds : 0 - let target: Double - if let duration { - target = min(max(currentSeconds + offset, 0), duration) - } else { - target = max(currentSeconds + offset, 0) - } - time = CMTime(seconds: target, preferredTimescale: 600) - } - - private func showSeekFeedback(_ feedback: SeekFeedback) { - seekFeedbackTask?.cancel() - seekFeedback = feedback - seekFeedbackOpacity = 0 - withAnimation(.easeOut(duration: 0.12)) { - seekFeedbackOpacity = 1 - } - seekFeedbackTask = Task { @MainActor in - try? await Task.sleep(nanoseconds: 260_000_000) - guard !Task.isCancelled else { return } - withAnimation(.easeIn(duration: 0.22)) { - seekFeedbackOpacity = 0 - } - try? await Task.sleep(nanoseconds: 240_000_000) - guard !Task.isCancelled else { return } - seekFeedback = nil - } - } - - private var duration: Double? { - switch videoState { - case .playing(let duration), .paused(let duration): - return duration > 0 ? duration : nil - default: - return nil - } - } - - private func beginFastPlayback() { - guard !isFastPlaybackActive else { return } - fastPlaybackWasPlaying = play - playbackResumeTask?.cancel() - play = false - playbackRate = fastPlaybackRate - isFastPlaybackActive = true - playbackResumeTask = Task { @MainActor in - try? await Task.sleep(nanoseconds: 16_000_000) - guard !Task.isCancelled else { return } - play = true - } - } - - private func endFastPlayback() { - playbackResumeTask?.cancel() - playbackResumeTask = nil - guard isFastPlaybackActive else { return } - let shouldResume = fastPlaybackWasPlaying - play = false - playbackRate = normalPlaybackRate - if shouldResume { - playbackResumeTask = Task { @MainActor in - try? await Task.sleep(nanoseconds: 16_000_000) - guard !Task.isCancelled else { return } - play = true - } - } - isFastPlaybackActive = false - } -} - -private enum SeekFeedback { - case backward - case forward - - var iconName: String { - switch self { - case .backward: - return "gobackward.5" - case .forward: - return "goforward.5" - } - } -} - -private struct VideoGestureOverlay: UIViewRepresentable { - let onDoubleTap: (CGFloat, CGFloat) -> Void - let onLongPressChanged: (Bool) -> Void - - func makeUIView(context: Context) -> UIView { - let view = WindowGestureHostView() - view.backgroundColor = .clear - view.onWindowChanged = { [weak coordinator = context.coordinator, weak view] in - coordinator?.installGestures(from: view) - } - return view - } - - func updateUIView(_ uiView: UIView, context: Context) { - context.coordinator.onDoubleTap = onDoubleTap - context.coordinator.onLongPressChanged = onLongPressChanged - DispatchQueue.main.async { - context.coordinator.installGestures(from: uiView) - } - } - - static func dismantleUIView(_ uiView: UIView, coordinator: Coordinator) { - coordinator.uninstallGestures() - } - - func makeCoordinator() -> Coordinator { - Coordinator(onDoubleTap: onDoubleTap, onLongPressChanged: onLongPressChanged) - } - - final class Coordinator: NSObject, UIGestureRecognizerDelegate { - var onDoubleTap: (CGFloat, CGFloat) -> Void - var onLongPressChanged: (Bool) -> Void - private weak var sourceView: UIView? - private weak var installedWindow: UIWindow? - private var doubleTapRecognizer: UITapGestureRecognizer? - private var longPressRecognizer: UILongPressGestureRecognizer? - private var longPressBeganInside = false - - init( - onDoubleTap: @escaping (CGFloat, CGFloat) -> Void, - onLongPressChanged: @escaping (Bool) -> Void - ) { - self.onDoubleTap = onDoubleTap - self.onLongPressChanged = onLongPressChanged - } - - func installGestures(from view: UIView?) { - sourceView = view - guard let window = view?.window, installedWindow !== window else { return } - uninstallGestures() - sourceView = view - - let doubleTap = UITapGestureRecognizer(target: self, action: #selector(handleDoubleTap(_:))) - doubleTap.numberOfTapsRequired = 2 - doubleTap.cancelsTouchesInView = false - doubleTap.delegate = self - - let longPress = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress(_:))) - longPress.minimumPressDuration = 0.35 - longPress.allowableMovement = 80 - longPress.cancelsTouchesInView = false - longPress.delegate = self - - window.addGestureRecognizer(doubleTap) - window.addGestureRecognizer(longPress) - installedWindow = window - doubleTapRecognizer = doubleTap - longPressRecognizer = longPress - } - - func uninstallGestures() { - if let doubleTapRecognizer { - installedWindow?.removeGestureRecognizer(doubleTapRecognizer) - } - if let longPressRecognizer { - installedWindow?.removeGestureRecognizer(longPressRecognizer) - } - installedWindow = nil - doubleTapRecognizer = nil - longPressRecognizer = nil - longPressBeganInside = false - } - - @objc func handleDoubleTap(_ recognizer: UITapGestureRecognizer) { - guard recognizer.state == .ended, - let window = recognizer.view, - let sourceView, - let location = localLocation(from: recognizer, in: window, sourceView: sourceView) else { return } - onDoubleTap(location.x, sourceView.bounds.width) - } - - @objc func handleLongPress(_ recognizer: UILongPressGestureRecognizer) { - guard let window = recognizer.view, - let sourceView else { return } - - switch recognizer.state { - case .began: - longPressBeganInside = localLocation(from: recognizer, in: window, sourceView: sourceView) != nil - if longPressBeganInside { - onLongPressChanged(true) - } - case .ended, .cancelled, .failed: - if longPressBeganInside { - onLongPressChanged(false) - } - longPressBeganInside = false - default: - break - } - } - - func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { - guard let sourceView, - let window = installedWindow else { return false } - let point = touch.location(in: window) - return sourceView.convert(sourceView.bounds, to: window).contains(point) - } - - func gestureRecognizer( - _ gestureRecognizer: UIGestureRecognizer, - shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer - ) -> Bool { - true - } - - private func localLocation( - from recognizer: UIGestureRecognizer, - in windowView: UIView, - sourceView: UIView - ) -> CGPoint? { - let pointInWindow = recognizer.location(in: windowView) - let sourceFrame = sourceView.convert(sourceView.bounds, to: windowView) - guard sourceFrame.contains(pointInWindow) else { return nil } - return sourceView.convert(pointInWindow, from: windowView) - } - } -} - -private final class WindowGestureHostView: UIView { - var onWindowChanged: (() -> Void)? - - override func didMoveToWindow() { - super.didMoveToWindow() - onWindowChanged?() - } - - override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { - false - } -} - @MainActor enum MediaOrientationController { static func setLandscape(_ enabled: Bool) { diff --git a/iosApp/flare/UI/Screen/StorageScreen.swift b/appleApp/ios/UI/Screen/StorageScreen.swift similarity index 96% rename from iosApp/flare/UI/Screen/StorageScreen.swift rename to appleApp/ios/UI/Screen/StorageScreen.swift index a9ada792e5..9b748bddd5 100644 --- a/iosApp/flare/UI/Screen/StorageScreen.swift +++ b/appleApp/ios/UI/Screen/StorageScreen.swift @@ -1,4 +1,5 @@ import SwiftUI +import AppleFontAwesome @preconcurrency import KotlinSharedUI import Kingfisher import UniformTypeIdentifiers @@ -6,6 +7,7 @@ import GSPlayer import VideoPlayer import Drops import WebKit +import FlareAppleCore struct StorageScreen: View { private let storagePresenter: StoragePresenter @@ -41,7 +43,7 @@ struct StorageScreen: View { Label { Text("storage_clear_image_cache") } icon: { - Image("fa-image") + Image(fontAwesome: .image) } } .alert("storage_clear_image_cache_confirm", isPresented: $showImageClearAlert) { @@ -81,7 +83,7 @@ struct StorageScreen: View { Label { Text("storage_clear_database_cache\(presenter.state.userCount) \(presenter.state.statusCount)") } icon: { - Image("fa-database") + Image(fontAwesome: .database) } } .alert("storage_clear_database_cache_confirm", isPresented: $showDatabaseClearAlert) { @@ -105,7 +107,7 @@ struct StorageScreen: View { Label { Text("storage_view_app_log") } icon: { - Image(.faEnvelope) + Image(fontAwesome: .envelope) } } @@ -125,7 +127,7 @@ struct StorageScreen: View { Text("settings_storage_export_data") Text("settings_storage_export_data_desc") } icon: { - Image("fa-file-export") + Image(fontAwesome: .fileExport) } } .fileExporter(isPresented: $showFileExporter, document: jsonFile, contentType: .json, defaultFilename: "flare_data_export") { result in @@ -145,7 +147,7 @@ struct StorageScreen: View { Text("settings_storage_import_data") Text("settings_storage_import_data_desc") } icon: { - Image("fa-file-import") + Image(fontAwesome: .fileImport) } } .fileImporter(isPresented: $showFileImporter, allowedContentTypes: [.json]) { result in diff --git a/iosApp/flare/UI/Screen/TabSettingsScreen.swift b/appleApp/ios/UI/Screen/TabSettingsScreen.swift similarity index 96% rename from iosApp/flare/UI/Screen/TabSettingsScreen.swift rename to appleApp/ios/UI/Screen/TabSettingsScreen.swift index 415a321520..dd0134e967 100644 --- a/iosApp/flare/UI/Screen/TabSettingsScreen.swift +++ b/appleApp/ios/UI/Screen/TabSettingsScreen.swift @@ -1,5 +1,8 @@ import SwiftUI +import FlareAppleUI import KotlinSharedUI +import FlareAppleCore +import AppleFontAwesome struct TabSettingsScreen: View { @StateObject private var presenter = KotlinPresenter(presenter: HomeTabSettingsPresenter()) @Environment(\.dismiss) private var dismiss @@ -56,7 +59,7 @@ struct TabSettingsScreen: View { editItem = item } } label: { - Image("fa-pen") + Image(fontAwesome: .pen) } .buttonStyle(.plain) Image(systemName: "line.3.horizontal") @@ -73,7 +76,7 @@ struct TabSettingsScreen: View { Label { Text("tab_settings_edit") } icon: { - Image("fa-pen") + Image(fontAwesome: .pen) } } } @@ -86,7 +89,7 @@ struct TabSettingsScreen: View { Label { Text("tab_settings_delete") } icon: { - Image("fa-trash") + Image(fontAwesome: .trash) } } } @@ -107,7 +110,7 @@ struct TabSettingsScreen: View { Button { dismiss() } label: { - Image("fa-xmark") + Image(fontAwesome: .xmark) } } ToolbarItem(placement: .primaryAction) { @@ -123,7 +126,7 @@ struct TabSettingsScreen: View { Text("tab_settings_add_tab") } } label: { - Image("fa-plus") + Image(fontAwesome: .plus) } } ToolbarItem(placement: .confirmationAction) { @@ -133,7 +136,7 @@ struct TabSettingsScreen: View { presenter.state.replaceHomeTimelineTabs(tabs: tabItems) dismiss() } label: { - Image("fa-check") + Image(fontAwesome: .check) } } } @@ -327,7 +330,7 @@ struct EditTabSheet: View { Label { Text("Close") } icon: { - Image("fa-xmark") + Image(fontAwesome: .xmark) } } } @@ -349,7 +352,7 @@ struct EditTabSheet: View { Label { Text("Done") } icon: { - Image("fa-check") + Image(fontAwesome: .check) } } } @@ -384,7 +387,7 @@ struct AddTabSheet: View { Label { Text("rss_add_source") } icon: { - Image("fa-plus") + Image(fontAwesome: .plus) } } .buttonStyle(.plain) @@ -392,7 +395,7 @@ struct AddTabSheet: View { Label { Text("rss_title") } icon: { - Image("fa-square-rss") + Image(fontAwesome: .squareRss) } } } @@ -430,7 +433,7 @@ struct AddTabSheet: View { Label { Text("Close") } icon: { - Image("fa-xmark") + Image(fontAwesome: .xmark) } } } @@ -485,7 +488,7 @@ struct AccountTabListView: View { Button { onDelete(item) } label: { - Image("fa-minus") + Image(fontAwesome: .minus) .foregroundColor(.red) } .buttonStyle(.plain) @@ -493,7 +496,7 @@ struct AccountTabListView: View { Button { onAdd(item) } label: { - Image("fa-plus") + Image(fontAwesome: .plus) .foregroundColor(.accentColor) } .buttonStyle(.plain) @@ -544,7 +547,7 @@ private struct AddTabRow: View { Button { onDelete(tabItem) } label: { - Image("fa-minus") + Image(fontAwesome: .minus) .foregroundColor(.red) } .buttonStyle(.plain) @@ -552,7 +555,7 @@ private struct AddTabRow: View { Button { onAdd(tabItem) } label: { - Image("fa-plus") + Image(fontAwesome: .plus) .foregroundColor(.accentColor) } .buttonStyle(.plain) diff --git a/iosApp/flare/UI/Screen/TimelineScreen.swift b/appleApp/ios/UI/Screen/TimelineScreen.swift similarity index 97% rename from iosApp/flare/UI/Screen/TimelineScreen.swift rename to appleApp/ios/UI/Screen/TimelineScreen.swift index 241e507e90..67b67d9908 100644 --- a/iosApp/flare/UI/Screen/TimelineScreen.swift +++ b/appleApp/ios/UI/Screen/TimelineScreen.swift @@ -1,5 +1,6 @@ import SwiftUI @preconcurrency import KotlinSharedUI +import FlareAppleCore struct TimelineScreen: View { let tabItem: UiTimelineTabItem diff --git a/iosApp/flare/UI/Screen/TwitterArticleScreen.swift b/appleApp/ios/UI/Screen/TwitterArticleScreen.swift similarity index 99% rename from iosApp/flare/UI/Screen/TwitterArticleScreen.swift rename to appleApp/ios/UI/Screen/TwitterArticleScreen.swift index 8f33310d8a..d0ebddec99 100644 --- a/iosApp/flare/UI/Screen/TwitterArticleScreen.swift +++ b/appleApp/ios/UI/Screen/TwitterArticleScreen.swift @@ -1,5 +1,7 @@ import SwiftUI +import FlareAppleUI import KotlinSharedUI +import FlareAppleCore struct TwitterArticleScreen: View { @StateObject private var presenter: KotlinPresenter diff --git a/iosApp/flare/UI/Screen/VVOStatusScreen.swift b/appleApp/ios/UI/Screen/VVOStatusScreen.swift similarity index 99% rename from iosApp/flare/UI/Screen/VVOStatusScreen.swift rename to appleApp/ios/UI/Screen/VVOStatusScreen.swift index e2e67d5d99..08da8da9b6 100644 --- a/iosApp/flare/UI/Screen/VVOStatusScreen.swift +++ b/appleApp/ios/UI/Screen/VVOStatusScreen.swift @@ -1,5 +1,7 @@ import SwiftUI +import FlareAppleUI import KotlinSharedUI +import FlareAppleCore struct VVOStatusScreen: View { @Environment(\.timelineAppearance.timelineDisplayMode) private var timelineDisplayMode diff --git a/iosApp/flare/UI/Screen/WebLoginScreen.swift b/appleApp/ios/UI/Screen/WebLoginScreen.swift similarity index 100% rename from iosApp/flare/UI/Screen/WebLoginScreen.swift rename to appleApp/ios/UI/Screen/WebLoginScreen.swift diff --git a/appleApp/macos/App/FlareApp.swift b/appleApp/macos/App/FlareApp.swift new file mode 100644 index 0000000000..a1a9559d01 --- /dev/null +++ b/appleApp/macos/App/FlareApp.swift @@ -0,0 +1,72 @@ +import AppKit +import FlareAppleCore +import KotlinSharedUI +import SwiftUI +import AppleFontAwesome + +@main +struct FlareApp: App { + init() { + AppleSharedHelper.shared.initialize( + inAppNotification: SwiftInAppNotification.shared, + swiftFormatter: Formatter.shared, + swiftPlatformTextRenderer: PlatformTextRenderer.shared, + swiftOnDeviceAI: FoundationModelOnDeviceAI.shared + ) + } + + var body: some Scene { + WindowGroup { + FlareTheme { + RootView() + } + } + .commands { + CommandGroup(replacing: .newItem) { + Button { + } label: { + Label { + Text("draft_box_title") + } icon: { + Image(fontAwesome: .penToSquare) + } + } + Button { + } label: { + Label { + Text("settings_rss_management_title") + } icon: { + Image(fontAwesome: .squareRss) + } + } + Button { + } label: { + Label { + Text("local_history_title") + } icon: { + Image(fontAwesome: .clockRotateLeft) + } + } + Button { + } label: { + Label { + Text("settings_agent_history_title") + } icon: { + Image(fontAwesome: .robot) + } + } + } + } +// .windowToolbarStyle(.unifiedCompact(showsTitle: false)) + Settings { + FlareTheme { + MacSettingsScreen() + } + } + .windowToolbarStyle(.unified) +// .defaultSize(width: 1120, height: 760) +// .commands { +// SidebarCommands() +// } + } +} diff --git a/appleApp/macos/App/RootView.swift b/appleApp/macos/App/RootView.swift new file mode 100644 index 0000000000..fb28dc650b --- /dev/null +++ b/appleApp/macos/App/RootView.swift @@ -0,0 +1,28 @@ +import FlareAppleCore +import KotlinSharedUI +import SwiftUI + +struct RootView: View { + @State private var selection: HomeTabsPresenterStateHomeTabs? = .home + @StateObject private var homeTabsPresenter = KotlinPresenter(presenter: HomeTabsPresenter()) + + var body: some View { + HStack(spacing: 0) { + SidebarView( + selection: $selection, + homeTabsPresenter: homeTabsPresenter + ) + DetailShell( + destination: selection ?? .home, + selection: $selection, + homeTabsPresenter: homeTabsPresenter + ) + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + .frame(minWidth: 300, minHeight: 620) + } +} + +#Preview { + RootView() +} diff --git a/appleApp/macos/AppIcon.icon/Assets/Group-2.svg b/appleApp/macos/AppIcon.icon/Assets/Group-2.svg new file mode 100644 index 0000000000..983932a98f --- /dev/null +++ b/appleApp/macos/AppIcon.icon/Assets/Group-2.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/appleApp/macos/AppIcon.icon/Assets/Group.svg b/appleApp/macos/AppIcon.icon/Assets/Group.svg new file mode 100644 index 0000000000..ad2f3e9a94 --- /dev/null +++ b/appleApp/macos/AppIcon.icon/Assets/Group.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/appleApp/macos/AppIcon.icon/icon.json b/appleApp/macos/AppIcon.icon/icon.json new file mode 100644 index 0000000000..f94fe0e12a --- /dev/null +++ b/appleApp/macos/AppIcon.icon/icon.json @@ -0,0 +1,68 @@ +{ + "fill" : { + "automatic-gradient" : "extended-srgb:0.00000,0.78431,0.70196,1.00000" + }, + "groups" : [ + { + "blur-material" : 0.5, + "layers" : [ + { + "glass" : true, + "hidden-specializations" : [ + { + "value" : true + }, + { + "appearance" : "dark", + "value" : false + } + ], + "image-name" : "Group.svg", + "name" : "Group", + "position" : { + "scale" : 3.18, + "translation-in-points" : [ + 0, + 0 + ] + } + }, + { + "blend-mode" : "normal", + "fill" : "automatic", + "glass" : true, + "hidden-specializations" : [ + { + "value" : false + }, + { + "appearance" : "dark", + "value" : true + } + ], + "image-name" : "Group-2.svg", + "name" : "Group-2", + "position" : { + "scale" : 3.18, + "translation-in-points" : [ + 0, + 0 + ] + } + } + ], + "lighting" : "individual", + "shadow" : { + "kind" : "neutral", + "opacity" : 0.5 + }, + "translucency" : { + "enabled" : true, + "value" : 0.5 + } + } + ], + "supported-platforms" : { + "squares" : "shared" + } +} \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-house.symbolset/Contents.json b/appleApp/macos/Assets.xcassets/AccentColor.colorset/Contents.json similarity index 63% rename from iosApp/flare/Assets.xcassets/fa-house.symbolset/Contents.json rename to appleApp/macos/Assets.xcassets/AccentColor.colorset/Contents.json index 5934a8ee25..eb87897008 100644 --- a/iosApp/flare/Assets.xcassets/fa-house.symbolset/Contents.json +++ b/appleApp/macos/Assets.xcassets/AccentColor.colorset/Contents.json @@ -1,12 +1,11 @@ { - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ + "colors" : [ { - "filename" : "house.svg", "idiom" : "universal" } - ] + ], + "info" : { + "author" : "xcode", + "version" : 1 + } } diff --git a/appleApp/macos/Assets.xcassets/Contents.json b/appleApp/macos/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/appleApp/macos/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/appleApp/macos/Common/LocalizedStrings.swift b/appleApp/macos/Common/LocalizedStrings.swift new file mode 100644 index 0000000000..e224a05826 --- /dev/null +++ b/appleApp/macos/Common/LocalizedStrings.swift @@ -0,0 +1,7 @@ +import Foundation + +enum LocalizedStrings { + static func string(_ key: String, fallback: String) -> String { + Bundle.main.localizedString(forKey: key, value: fallback, table: nil) + } +} diff --git a/appleApp/macos/Common/SwiftInAppNotification.swift b/appleApp/macos/Common/SwiftInAppNotification.swift new file mode 100644 index 0000000000..04cf3af8d1 --- /dev/null +++ b/appleApp/macos/Common/SwiftInAppNotification.swift @@ -0,0 +1,17 @@ +import Foundation +import KotlinSharedUI + +final class SwiftInAppNotification: InAppNotification { + private init() {} + + static let shared = SwiftInAppNotification() + + func onError(message: Message, throwable: KotlinThrowable) { + } + + func onProgress(message: Message, progress: Int32, total: Int32) { + } + + func onSuccess(message: Message) { + } +} diff --git a/appleApp/macos/Flare.entitlements b/appleApp/macos/Flare.entitlements new file mode 100644 index 0000000000..625af03d99 --- /dev/null +++ b/appleApp/macos/Flare.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.files.user-selected.read-only + + com.apple.security.network.client + + + diff --git a/appleApp/macos/Info.plist b/appleApp/macos/Info.plist new file mode 100644 index 0000000000..8e65de93da --- /dev/null +++ b/appleApp/macos/Info.plist @@ -0,0 +1,42 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Flare + CFBundleURLTypes + + + CFBundleTypeRole + Viewer + CFBundleURLName + dev.dimension.flare + CFBundleURLSchemes + + flare + pixiv + + + + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + $(MARKETING_VERSION) + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + LSApplicationCategoryType + public.app-category.social-networking + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + + diff --git a/appleApp/macos/UI/Component/TimelinePagingView.swift b/appleApp/macos/UI/Component/TimelinePagingView.swift new file mode 100644 index 0000000000..a6b735e9cc --- /dev/null +++ b/appleApp/macos/UI/Component/TimelinePagingView.swift @@ -0,0 +1,44 @@ +import FlareAppleUI +import KotlinSharedUI +import SwiftUI + +struct TimelinePagingContent: View { + @Environment(\.refresh) private var refreshAction: RefreshAction? + + let data: PagingState + let detailStatusKey: MicroBlogKey? + let key: String + let topContentInset: CGFloat + let allowGalleryMode: Bool + let suppressInitialRefreshIndicator: Bool + + init( + data: PagingState, + detailStatusKey: MicroBlogKey?, + key: String, + topContentInset: CGFloat = 0, + allowGalleryMode: Bool = false, + suppressInitialRefreshIndicator: Bool = false + ) { + self.data = data + self.detailStatusKey = detailStatusKey + self.key = key + self.topContentInset = topContentInset + self.allowGalleryMode = allowGalleryMode + self.suppressInitialRefreshIndicator = suppressInitialRefreshIndicator + } + + var body: some View { + ScrollView { + TimelinePagingView(data: data, detailStatusKey: detailStatusKey) + .padding(.top, topContentInset) + .padding(.bottom, 12) + } + .id(key) + .refreshable { + if let refreshAction { + await refreshAction() + } + } + } +} diff --git a/appleApp/macos/UI/FlareTheme.swift b/appleApp/macos/UI/FlareTheme.swift new file mode 100644 index 0000000000..9645d741e0 --- /dev/null +++ b/appleApp/macos/UI/FlareTheme.swift @@ -0,0 +1,76 @@ +import AppKit +import FlareAppleCore +import FlareAppleUI +import KotlinSharedUI +import SwiftUI + +struct FlareTheme: View { + @ViewBuilder let content: () -> Content + + @StateObject private var presenter = KotlinPresenter(presenter: EnvironmentSettingsPresenter()) + @State private var appSettings: AppSettings = AppSettings(version: "0") + @State private var globalAppearance: GlobalAppearance = GlobalAppearance.companion.Default + @State private var timelineAppearance: TimelineAppearance = TimelineAppearance.companion.Default + + private let sizes: [DynamicTypeSize] = [.xSmall, .small, .medium, .large, .xLarge, .xxLarge, .xxxLarge] + + var body: some View { + content() + .environment(\.aiConfig, appSettings.aiConfig) + .environment(\.translateConfig, appSettings.translateConfig) + .environment(\.globalAppearance, globalAppearance) + .environment(\.timelineAppearance, timelineAppearance) + .preferredColorScheme( + globalAppearance.theme == .system ? nil : (globalAppearance.theme == .dark ? .dark : .light) + ) + .dynamicTypeSize(sizes[min(max(Int(globalAppearance.fontSizeDiff) + 2, 0), sizes.count - 1)]) + .environment(\.openURL, OpenURLAction { url in + .systemAction(url) + }) + .onSuccessOf(of: presenter.state.appSettings) { newValue in + appSettings = newValue + timelineAppearance = timelineAppearance.withAppSettings(newValue) + } + .onSuccessOf(of: presenter.state.globalAppearance) { newValue in + globalAppearance = newValue + } + .onSuccessOf(of: presenter.state.timelineAppearance) { newValue in + timelineAppearance = newValue.withAppSettings(appSettings) + } + } +} + +private extension TimelineAppearance { + func withAppSettings(_ appSettings: AppSettings) -> TimelineAppearance { + doCopy( + avatarShape: avatarShape, + showMedia: showMedia, + showSensitiveContent: showSensitiveContent, + expandContentWarning: expandContentWarning, + expandMediaSize: expandMediaSize, + videoAutoplay: videoAutoplay, + showLinkPreview: showLinkPreview, + compatLinkPreview: compatLinkPreview, + showNumbers: showNumbers, + postActionStyle: postActionStyle, + postActionLayout: postActionLayout, + fullWidthPost: fullWidthPost, + absoluteTimestamp: absoluteTimestamp, + showPlatformLogo: showPlatformLogo, + timelineDisplayMode: timelineDisplayMode, + aiConfig: TimelineAppearance.AiConfig( + translation: true, + tldr: appSettings.aiConfig.tldr, + agent: appSettings.aiConfig.agent && appSettings.aiConfig.type.openAIModel?.isEmpty == false + ), + lineLimit: lineLimit, + showTranslateButton: showTranslateButton + ) + } +} + +private extension AppSettingsAiConfigType { + var openAIModel: String? { + (self as? AppSettingsAiConfigTypeOpenAI)?.model + } +} diff --git a/appleApp/macos/UI/Navigation/DetailShell.swift b/appleApp/macos/UI/Navigation/DetailShell.swift new file mode 100644 index 0000000000..24c78f11f2 --- /dev/null +++ b/appleApp/macos/UI/Navigation/DetailShell.swift @@ -0,0 +1,57 @@ +import AppKit +import FlareAppleCore +import KotlinSharedUI +import SwiftUI +import AppleFontAwesome +import SwiftUIIntrospect + +struct DetailShell: View { + let destination: HomeTabsPresenterStateHomeTabs + @Binding var selection: HomeTabsPresenterStateHomeTabs? + @ObservedObject var homeTabsPresenter: KotlinPresenter + + var body: some View { + StateView(state: homeTabsPresenter.state.tabs) { tabs in + TabView(selection: $selection) { + ForEach(tabs.cast(HomeTabsPresenterStateHomeTabs.self), id: \.name) { tab in + Router( + initialRoute: tab.macOSInitialRoute, + isActive: selection?.name == tab.name + ) + .tag(tab) + .tabItem { + Label { + Text(tab.macOSTitle) + } icon: { + Image(fontAwesome: tab.macOSIcon) + } + } + } + } + .introspect(.window, on: .macOS(.v15, .v26, .v27)) { window in + guard let toolbar = window.toolbar else { return } + for item in toolbar.items { + if item.label == "Navigation Tab Bar" { + // Remove border + item.isBordered = false + // Hide item instead of removing to avoid system recreation + item.view?.isHidden = true + // Set width to 0 using constraints to prevent occupying toolbar space + item.view?.widthAnchor.constraint(equalToConstant: 0).isActive = true + } + } + } + } errorContent: { _ in + PlaceholderPanel(destination: selectedTab) + } loadingContent: { + PlaceholderPanel(destination: selectedTab) + .redacted(reason: .placeholder) + } + } + + private var selectedTab: HomeTabsPresenterStateHomeTabs { + selection ?? destination + } + + +} diff --git a/appleApp/macos/UI/Navigation/HomeTabsPresenterStateHomeTabs+MacPresentation.swift b/appleApp/macos/UI/Navigation/HomeTabsPresenterStateHomeTabs+MacPresentation.swift new file mode 100644 index 0000000000..33ef3d47ae --- /dev/null +++ b/appleApp/macos/UI/Navigation/HomeTabsPresenterStateHomeTabs+MacPresentation.swift @@ -0,0 +1,57 @@ +import AppleFontAwesome +import KotlinSharedUI + +extension HomeTabsPresenterStateHomeTabs { + var macOSInitialRoute: Route { + switch self { + case .home: + .home + case .notifications: + .notification + case .discover: + .discover + } + } + + var macOSTitle: String { + switch self { + case .home: + LocalizedStrings.string("home_tab_home_title", fallback: "Home") + case .notifications: + LocalizedStrings.string("home_tab_notifications_title", fallback: "Notifications") + case .discover: + LocalizedStrings.string("home_tab_discover_title", fallback: "Discover") + } + } + + var macOSIcon: FontAwesomeIcon { + switch self { + case .home: + .house + case .notifications: + .bell + case .discover: + .magnifyingGlass + } + } + + var macOSPlaceholder: String { + switch self { + case .home: + LocalizedStrings.string("macos_placeholder_home", fallback: "Timeline content will render here.") + case .notifications: + LocalizedStrings.string("macos_placeholder_notifications", fallback: "Notification content will render here.") + case .discover: + LocalizedStrings.string("macos_placeholder_discover", fallback: "Search and discovery content will render here.") + } + } + + var macOSShowsTimelineSkeleton: Bool { + switch self { + case .home, .notifications: + true + case .discover: + false + } + } +} diff --git a/appleApp/macos/UI/Navigation/Route.swift b/appleApp/macos/UI/Navigation/Route.swift new file mode 100644 index 0000000000..692d660aee --- /dev/null +++ b/appleApp/macos/UI/Navigation/Route.swift @@ -0,0 +1,154 @@ +import KotlinSharedUI +import SwiftUI +import FlareAppleCore +import FlareAppleUI + +enum Route: Hashable, Identifiable { + case empty + case home + case notification + case discover + case serviceSelect + case timeline(UiTimelineTabItem) + case statusDetail(AccountType, MicroBlogKey) + case profileUser(AccountType, MicroBlogKey) + case profileUserNameWithHost(AccountType, String, String) + case userFollowing(AccountType, MicroBlogKey) + case userFans(AccountType, MicroBlogKey) + case deepLinkAccountPicker(String, [MicroBlogKey: Route]) + case externalLink(String) + + var id: Int { + return self.hashValue + } + + static func == (lhs: Route, rhs: Route) -> Bool { + switch (lhs, rhs) { + case (.timeline(let lhs), .timeline(let rhs)): + return lhs.id == rhs.id + default: + return lhs.hashValue == rhs.hashValue + } + } + + func hash(into hasher: inout Hasher) { + switch self { + case .timeline(let item): + hasher.combine("timeline") + hasher.combine(item.id) + default: + hasher.combine(String(describing: self)) + } + } + + @MainActor + @ViewBuilder + func view( + onNavigate: @escaping (Route) -> Void, + goBack: @escaping () -> Void + ) -> some View { + switch self { + case .empty: + EmptyView() + case .home: + HomeScreen() + case .notification: + PlaceholderPanel(destination: .notifications) + case .discover: + PlaceholderPanel(destination: .discover) + case .serviceSelect: + ServiceSelectionScreen(toHome: goBack) + case .timeline(let item): + TimelineScreen(tabItem: item, allowGalleryMode: true) + .navigationTitle(item.title.text) + case .statusDetail(let accountType, let statusKey): + StatusDetailScreen(accountType: accountType, statusKey: statusKey) + case .profileUser(let accountType, let userKey): + ProfileScreen( + accountType: accountType, + userKey: userKey, + onFollowingClick: { key in onNavigate(.userFollowing(accountType, key)) }, + onFansClick: { key in onNavigate(.userFans(accountType, key)) }, + goBack: goBack + ) + case .profileUserNameWithHost(let accountType, let userName, let host): + ProfileWithUserNameAndHostScreen( + userName: userName, + host: host, + accountType: accountType, + onFollowingClick: { key in onNavigate(.userFollowing(accountType, key)) }, + onFansClick: { key in onNavigate(.userFans(accountType, key)) }, + goBack: goBack + ) + case .userFollowing(let accountType, let userKey): + UserListScreen(accountType: accountType, userKey: userKey, isFollowing: true) + case .userFans(let accountType, let userKey): + UserListScreen(accountType: accountType, userKey: userKey, isFollowing: false) + case .deepLinkAccountPicker(let originalUrl, let data): + DeepLinkAccountPickerView( + originalUrl: originalUrl, + data: data, + onNavigate: onNavigate + ) + case .externalLink: + EmptyView() + } + } +} + +extension Route { + static func fromDeepLinkRoute(deeplinkRoute: DeeplinkRoute) -> Route? { + switch onEnum(of: deeplinkRoute) { + case .login: + .serviceSelect + case .timeline(let data): + fromTimeline(data) + case .status(let status): + fromStatus(status) + case .profile(let profile): + fromProfile(profile) + case .deepLinkAccountPicker(let picker): + fromAccountPicker(picker) + case .openLinkDirectly(let data): + .externalLink(data.url) + default: + .empty + } + } + + private static func fromTimeline(_ timeline: DeeplinkRoute.Timeline) -> Route? { + switch onEnum(of: timeline) { + case .xQTDeviceFollow(let data): + if let tabItem = XQTUiTimelineTabItemHelpers.shared.xqtDeviceFollow(accountType: data.accountType) { + .timeline(tabItem) + } else { + nil + } + } + } + + private static func fromProfile(_ profile: DeeplinkRoute.Profile) -> Route? { + switch onEnum(of: profile) { + case .user(let data): + .profileUser(data.accountType, data.userKey) + case .userNameWithHost(let data): + .profileUserNameWithHost(data.accountType, data.userName, data.host) + } + } + + private static func fromStatus(_ status: DeeplinkRoute.Status) -> Route? { + switch onEnum(of: status) { + case .detail(let data): + .statusDetail(data.accountType, data.statusKey) + default: + .empty + } + } + + private static func fromAccountPicker(_ picker: DeeplinkRoute.DeepLinkAccountPicker) -> Route? { + let routes = picker.data.mapValues { route in + fromDeepLinkRoute(deeplinkRoute: route) ?? .empty + } + return .deepLinkAccountPicker(picker.originalUrl, routes) + } +} diff --git a/appleApp/macos/UI/Navigation/Router.swift b/appleApp/macos/UI/Navigation/Router.swift new file mode 100644 index 0000000000..435f9e8879 --- /dev/null +++ b/appleApp/macos/UI/Navigation/Router.swift @@ -0,0 +1,143 @@ +import Combine +import FlareAppleCore +import KotlinSharedUI +import SwiftUI +import SwiftUIBackports + +struct Router: View { + @Environment(\.openURL) private var openURL + let initialRoute: Route + let isActive: Bool + @State private var backStack: [Route] = [] + @State private var sheet: Route? + @StateObject private var deepLinkPresenter: KotlinPresenter + @StateObject private var deepLinkHandler: MacDeepLinkHandler + + init( + initialRoute: Route, + isActive: Bool = true + ) { + self.initialRoute = initialRoute + self.isActive = isActive + let handler = MacDeepLinkHandler() + _deepLinkHandler = .init(wrappedValue: handler) + _deepLinkPresenter = .init( + wrappedValue: .init( + presenter: DeepLinkPresenter( + onRoute: { [weak handler] deeplinkRoute in + if let route = Route.fromDeepLinkRoute(deeplinkRoute: deeplinkRoute) { + handler?.onRoute?(route) + } + }, + onLink: { [weak handler] link in + handler?.onLink?(link) + } + ) + ) + ) + } + + var body: some View { + NavigationStack(path: $backStack.animation()) { + initialRoute.view( + onNavigate: handle(route:), + goBack: {} + ) + .navigationDestination(for: Route.self) { route in + route.view( + onNavigate: handle(route:), + goBack: goBack + ) + .background(Color(nsColor: .windowBackgroundColor)) + } + .backport + .navigationTransitionAutomatic() + } + .sheet(item: $sheet) { route in + NavigationStack { + route.view( + onNavigate: handle(route:), + goBack: { + sheet = nil + } + ) + } + .frame(minWidth: 380, minHeight: 420) + } + .environment(\.openURL, OpenURLAction { url in + deepLinkPresenter.state.handle(url: url.absoluteString) + return .handled + }) + .onOpenURL { url in + if isActive { + deepLinkPresenter.state.handle(url: url.absoluteString) + } + } + .onAppear { + deepLinkHandler.onRoute = { route in + handle(route: route) + } + deepLinkHandler.onLink = { link in + if let url = URL(string: link) { + openURL(url) + } + } + } + } + + private func navigate(_ route: Route) { + if backStack.last != route { + backStack.append(route) + } + } + + private func handle(route: Route) { + switch route { + case .externalLink(let link): + if let url = URL(string: link) { + openURL(url) + } + case _ where route == initialRoute: + backStack.removeAll() + sheet = nil + default: + if isSheetRoute(route) { + sheet = route + } else { + navigate(route) + sheet = nil + } + } + } + + private func goBack() { + if !backStack.isEmpty { + backStack.removeLast() + } + } + + private func isSheetRoute(_ route: Route) -> Bool { + switch route { + case .serviceSelect, .deepLinkAccountPicker: + true + default: + false + } + } +} + +final class MacDeepLinkHandler: ObservableObject { + var onRoute: ((Route) -> Void)? + var onLink: ((String) -> Void)? +} + +public extension Backport where Content: View { + @ViewBuilder + func navigationTransitionAutomatic() -> some View { + if #available(macOS 15.0, *) { + content.navigationTransition(.automatic) + } else { + content + } + } +} diff --git a/appleApp/macos/UI/Navigation/SidebarMaterialBackground.swift b/appleApp/macos/UI/Navigation/SidebarMaterialBackground.swift new file mode 100644 index 0000000000..e4a039bf83 --- /dev/null +++ b/appleApp/macos/UI/Navigation/SidebarMaterialBackground.swift @@ -0,0 +1,18 @@ +import AppKit +import SwiftUI + +struct SidebarMaterialBackground: NSViewRepresentable { + func makeNSView(context: Context) -> NSVisualEffectView { + let view = NSVisualEffectView() + view.material = .sidebar + view.blendingMode = .behindWindow + view.state = .followsWindowActiveState + return view + } + + func updateNSView(_ nsView: NSVisualEffectView, context: Context) { + nsView.material = .sidebar + nsView.blendingMode = .behindWindow + nsView.state = .followsWindowActiveState + } +} diff --git a/appleApp/macos/UI/Navigation/SidebarView.swift b/appleApp/macos/UI/Navigation/SidebarView.swift new file mode 100644 index 0000000000..ecbace8fea --- /dev/null +++ b/appleApp/macos/UI/Navigation/SidebarView.swift @@ -0,0 +1,216 @@ +import AppleFontAwesome +import FlareAppleCore +import KotlinSharedUI +import SwiftUI + +struct SidebarView: View { + @Binding var selection: HomeTabsPresenterStateHomeTabs? + @ObservedObject var homeTabsPresenter: KotlinPresenter + @State private var isLoginSheetPresented = false + @StateObject private var notificationBadgePresenter = KotlinPresenter( + presenter: AllNotificationBadgePresenter()) + @StateObject private var loggedInPresenter = KotlinPresenter(presenter: LoggedInPresenter()) + @StateObject private var canComposePresenter = KotlinPresenter(presenter: CanComposePresenter()) + + @Environment(\.openSettings) private var openSettings + + var body: some View { + VStack(spacing: 0) { + ScrollView { + VStack(spacing: 8) { + if shouldShowLogin { + SidebarIconButton( + title: LocalizedStrings.string("login_button", fallback: "Log in"), + icon: .userPlus, + isSelected: isLoginSheetPresented + ) { + isLoginSheetPresented = true + } + + Divider() + .frame(width: 32) + .padding(.vertical, 2) + } + + StateView(state: homeTabsPresenter.state.tabs) { tabs in + ForEach(tabs.cast(HomeTabsPresenterStateHomeTabs.self), id: \.name) { tab in + SidebarIconButton( + title: tab.macOSTitle, + icon: tab.macOSIcon, + badge: notificationBadge(for: tab), + isSelected: isSelected(tab) + ) { + withAnimation { + selection = tab + } + } + } + } loadingContent: { + SidebarLoadingItems() + } + + if canCompose { + SidebarIconButton( + title: LocalizedStrings.string("home_compose", fallback: "Compose"), + icon: .pen, + isSelected: false, + isAccent: true + ) { + } + .padding(.top, 4) + } + } + .frame(maxWidth: .infinity) + .padding(.top, 12) + .padding(.bottom, 8) + } + } + .frame(width: 72) + .frame(maxHeight: .infinity, alignment: .top) + .background { + SidebarMaterialBackground() + .ignoresSafeArea(.container, edges: [.top, .bottom]) + } + .overlay(alignment: .trailing) { + Divider() + .ignoresSafeArea(.container, edges: [.top, .bottom]) + } + .sheet(isPresented: $isLoginSheetPresented) { + NavigationStack { + ServiceSelectionScreen { + isLoginSheetPresented = false + selection = .home + } + .navigationTitle(LocalizedStrings.string("login_button", fallback: "Log in")) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button { + isLoginSheetPresented = false + } label: { + Label { + Text(LocalizedStrings.string("compose_button_cancel", fallback: "Cancel")) + } icon: { + Image(systemName: "xmark") + } + } + .help(LocalizedStrings.string("compose_button_cancel", fallback: "Cancel")) + } + } + } + .frame(width: 380, height: 480) + } + } + + private var shouldShowLogin: Bool { + if case .success(let state) = onEnum(of: loggedInPresenter.state.isLoggedIn) { + !state.data.boolValue + } else { + false + } + } + + private var notificationBadge: Int { + Int(notificationBadgePresenter.state.count) + } + + private var canCompose: Bool { + if case .success(let state) = onEnum(of: canComposePresenter.state.canCompose) { + state.data.boolValue + } else { + false + } + } + + private func isSelected(_ tab: HomeTabsPresenterStateHomeTabs) -> Bool { + selection?.name == tab.name + } + + private func notificationBadge(for tab: HomeTabsPresenterStateHomeTabs) -> Int { + switch tab { + case .notifications: + notificationBadge + case .home, .discover: + 0 + } + } +} + +private struct SidebarIconButton: View { + let title: String + let icon: FontAwesomeIcon + var badge: Int = 0 + let isSelected: Bool + var isAccent: Bool = false + let action: () -> Void + + var body: some View { + Button(action: action) { + ZStack(alignment: .topTrailing) { + Image(fontAwesome: icon) + .resizable() + .scaledToFit() + .foregroundStyle(foregroundColor) + .frame(width: 20, height: 20) + .frame(width: 44, height: 44) + + if badge > 0 { + Text(badgeText) + .font(.system(size: 10, weight: .semibold)) + .foregroundStyle(.white) + .lineLimit(1) + .minimumScaleFactor(0.7) + .padding(.horizontal, 4) + .frame(minWidth: 16, minHeight: 16) + .background(.red, in: Capsule()) + .offset(x: 4, y: -3) + } + } + .frame(width: 44, height: 44) + .background(backgroundColor, in: .circle) + .shadow(color: shadowColor, radius: isAccent ? 4 : 0, y: isAccent ? 2 : 0) + .contentShape(Circle()) + } + .buttonStyle(.plain) + .help(title) + .accessibilityLabel(title) + } + + private var backgroundColor: Color { + if isAccent { + .accentColor + } else if isSelected { + .clear + } else { + .clear + } + } + + private var foregroundColor: Color { + if isAccent { + .white + } else if isSelected { + .accentColor + } else { + .secondary + } + } + + private var shadowColor: Color { + isAccent ? .black.opacity(0.22) : .clear + } + + private var badgeText: String { + badge > 99 ? "99+" : "\(badge)" + } +} + +private struct SidebarLoadingItems: View { + var body: some View { + ForEach(0..<2, id: \.self) { _ in + Circle() + .fill(.quaternary) + .frame(width: 44, height: 44) + .redacted(reason: .placeholder) + } + } +} diff --git a/appleApp/macos/UI/Screen/HomeScreen.swift b/appleApp/macos/UI/Screen/HomeScreen.swift new file mode 100644 index 0000000000..a633dd124e --- /dev/null +++ b/appleApp/macos/UI/Screen/HomeScreen.swift @@ -0,0 +1,139 @@ +import AppleFontAwesome +import FlareAppleCore +import FlareAppleUI +import KotlinSharedUI +import SwiftUI +import SwiftUIBackports + +struct HomeScreen: View { + @StateObject private var presenter = KotlinPresenter( + presenter: HomeTimelineWithTabsPresenter() + ) + @Environment(\.timelineAppearance) private var timelineAppearance + @State private var selectedTabID: String? + + var body: some View { + StateView(state: presenter.state.tabState) { state in + let tabs = state.cast(UiTimelineTabItem.self) + content(tabs: tabs) + } errorContent: { _ in + content(tabs: []) + } loadingContent: { + content(tabs: []) + .redacted(reason: .placeholder) + } + } + + private func content(tabs: [UiTimelineTabItem]) -> some View { + ZStack { + if let tab = selectedTab(in: tabs) { + TimelineScreen(tabItem: tab, allowGalleryMode: true) + .environment(\.timelineAppearance, tab.resolveTimelineAppearance(base: timelineAppearance)) + .id(tab.id) + } else if tabs.isEmpty { + PlaceholderPanel(destination: .home) + } + } + .toolbar { + ToolbarItem(placement: .navigation) { + HomeTimelineTabPicker( + tabs: tabs, + selectedTabID: $selectedTabID + ) + } + ToolbarItem(placement: .primaryAction) { + Button { + } label: { + Image(fontAwesome: .sliders) + } + .help(LocalizedStrings.string("settings_title", fallback: "Settings")) + } + ToolbarItem(placement: .primaryAction) { + Button { + } label: { + Image(fontAwesome: .arrowsRotate) + } + .help(LocalizedStrings.string("refresh", fallback: "Refresh")) + } + } + } + + private func selectedTab(in tabs: [UiTimelineTabItem]) -> UiTimelineTabItem? { + if let selectedTabID, let selected = tabs.first(where: { $0.id == selectedTabID }) { + return selected + } + return tabs.first + } +} + +private struct HomeTimelineTabPicker: View { + let tabs: [UiTimelineTabItem] + @Binding var selectedTabID: String? + + var body: some View { + Picker(selection: selectedTabSelection) { + if tabs.isEmpty { + Text(selectedTabTitle) + .tag("") + } else { + ForEach(tabs, id: \.id) { tab in + Text(tab.title.text) + .tag(tab.id) + } + } + } label: { + Text(selectedTabTitle) + } + .pickerStyle(.menu) + .labelsHidden() + .font(.headline) + .fixedSize() + .disabled(tabs.isEmpty) + .onChange(of: tabIDs) { _, ids in + normalizeSelection(with: ids) + } + .onAppear { + normalizeSelection(with: tabIDs) + } + } + + private var selectedTabSelection: Binding { + Binding { + selectedTab?.id ?? tabs.first?.id ?? "" + } set: { id in + selectedTabID = id.isEmpty ? nil : id + } + } + + private var selectedTabTitle: String { + selectedTab?.title.text + ?? tabs.first?.title.text + ?? LocalizedStrings.string("home_tab_home_title", fallback: "Home") + } + + private var selectedTab: UiTimelineTabItem? { + guard let selectedTabID else { return nil } + return tabs.first { $0.id == selectedTabID } + } + + private var tabIDs: [String] { + tabs.map(\.id) + } + + private func normalizeSelection(with ids: [String]) { + if ids.isEmpty { + selectedTabID = nil + } else if selectedTabID == nil || !ids.contains(selectedTabID ?? "") { + selectedTabID = ids.first + } + } +} + +private struct HomeTimelineTabLoadingPicker: View { + var body: some View { + Text(LocalizedStrings.string("macos_loading", fallback: "Loading")) + .font(.headline) + .foregroundStyle(.secondary) + .redacted(reason: .placeholder) + } +} diff --git a/appleApp/macos/UI/Screen/MacSettingsScreen.swift b/appleApp/macos/UI/Screen/MacSettingsScreen.swift new file mode 100644 index 0000000000..84065cfe4c --- /dev/null +++ b/appleApp/macos/UI/Screen/MacSettingsScreen.swift @@ -0,0 +1,2072 @@ +import AppKit +import AppleFontAwesome +import FlareAppleCore +import FlareAppleUI +import Kingfisher +@preconcurrency import KotlinSharedUI +import SwiftUI +import UniformTypeIdentifiers +import WebKit + +struct MacSettingsScreen: View { + @State private var selectedPane: MacSettingsPane = .accountManagement + + var body: some View { + TabView(selection: $selectedPane) { + ForEach(MacSettingsPane.allCases) { pane in + NavigationStack { + pane.detail + } + .tabItem { + Label { + Text(pane.title) + } icon: { + Image(fontAwesome: pane.icon) + } + } + .tag(pane) + } + } + .frame(minWidth: 880, minHeight: 620) + } +} + +private enum MacSettingsPane: String, CaseIterable, Identifiable, Hashable { + case accountManagement + case appearance + case localFilter + case storage + case aiConfig + case translationConfig + case about + + var id: String { rawValue } + + var title: LocalizedStringKey { + switch self { + case .accountManagement: + "account_management_title" + case .appearance: + "macos_settings_section_appearance" + case .localFilter: + "local_filter_title" + case .storage: + "storage_title" + case .aiConfig: + "ai_config_title" + case .translationConfig: + "settings_translation_title" + case .about: + "about_title" + } + } + + var subtitle: LocalizedStringKey { + switch self { + case .accountManagement: + "account_management_description" + case .appearance: + "appearance_description" + case .localFilter: + "local_filter_description" + case .storage: + "storage_description" + case .aiConfig: + "ai_config_description" + case .translationConfig: + "settings_translation_description" + case .about: + "about_description" + } + } + + var icon: FontAwesomeIcon { + switch self { + case .accountManagement: + .circleUser + case .appearance: + .palette + case .localFilter: + .filter + case .storage: + .database + case .aiConfig: + .robot + case .translationConfig: + .language + case .about: + .circleInfo + } + } + + @ViewBuilder + var detail: some View { + switch self { + case .accountManagement: + MacAccountManagementSettingsPane() + case .appearance: + MacAppearanceSettingsPane() + case .localFilter: + MacLocalFilterSettingsPane() + case .storage: + MacStorageSettingsPane() + case .aiConfig: + MacAiConfigSettingsPane() + case .translationConfig: + MacTranslationConfigSettingsPane() + case .about: + MacAboutSettingsPane() + } + } +} + +private struct MacSettingsForm: View { + let title: LocalizedStringKey + let subtitle: LocalizedStringKey + @ViewBuilder let content: () -> Content + + var body: some View { + Form { + content() + } + .formStyle(.grouped) + .navigationTitle(title) + .navigationSubtitle(Text(subtitle)) + } +} + +private struct MacSettingsEmptyState: View { + let pageTitle: LocalizedStringKey + let pageSubtitle: LocalizedStringKey + let title: LocalizedStringKey + let systemImage: String + let description: LocalizedStringKey? + + init( + pageTitle: LocalizedStringKey, + pageSubtitle: LocalizedStringKey, + title: LocalizedStringKey, + systemImage: String, + description: LocalizedStringKey? = nil + ) { + self.pageTitle = pageTitle + self.pageSubtitle = pageSubtitle + self.title = title + self.systemImage = systemImage + self.description = description + } + + var body: some View { + if let description { + ContentUnavailableView( + title, + systemImage: systemImage, + description: Text(description) + ) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .navigationTitle(pageTitle) + .navigationSubtitle(Text(pageSubtitle)) + } else { + ContentUnavailableView(title, systemImage: systemImage) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .navigationTitle(pageTitle) + .navigationSubtitle(Text(pageSubtitle)) + } + } +} + +private struct MacSettingsLoadingState: View { + let title: LocalizedStringKey + let subtitle: LocalizedStringKey + + var body: some View { + ProgressView() + .frame(maxWidth: .infinity, maxHeight: .infinity) + .navigationTitle(title) + .navigationSubtitle(Text(subtitle)) + } +} + +private struct MacSettingLabel: View { + let title: LocalizedStringKey + let subtitleKey: LocalizedStringKey? + let value: String? + + init(_ title: LocalizedStringKey, subtitle: LocalizedStringKey? = nil) { + self.title = title + self.subtitleKey = subtitle + self.value = nil + } + + init(_ title: LocalizedStringKey, value: String) { + self.title = title + self.subtitleKey = nil + self.value = value + } + + var body: some View { + MacSettingLabelContent(title: title, subtitleKey: subtitleKey, value: value) + } +} + +private struct MacSettingActionRow: View { + let title: LocalizedStringKey + let subtitleKey: LocalizedStringKey? + let value: String? + let buttonTitle: LocalizedStringKey + let icon: FontAwesomeIcon + let role: ButtonRole? + let action: () -> Void + + init( + _ title: LocalizedStringKey, + subtitle: LocalizedStringKey? = nil, + buttonTitle: LocalizedStringKey, + icon: FontAwesomeIcon, + role: ButtonRole? = nil, + action: @escaping () -> Void + ) { + self.title = title + self.subtitleKey = subtitle + self.value = nil + self.buttonTitle = buttonTitle + self.icon = icon + self.role = role + self.action = action + } + + init( + _ title: LocalizedStringKey, + value: String, + buttonTitle: LocalizedStringKey, + icon: FontAwesomeIcon, + role: ButtonRole? = nil, + action: @escaping () -> Void + ) { + self.title = title + self.subtitleKey = nil + self.value = value + self.buttonTitle = buttonTitle + self.icon = icon + self.role = role + self.action = action + } + + var body: some View { + HStack(spacing: 16) { + MacSettingLabelContent(title: title, subtitleKey: subtitleKey, value: value) + .frame(maxWidth: .infinity, alignment: .leading) + + Button(role: role, action: action) { + Label { + Text(buttonTitle) + } icon: { + Image(fontAwesome: icon) + } + } + .buttonStyle(.bordered) + .controlSize(.small) + } + } +} + +private struct MacSettingLabelContent: View { + let title: LocalizedStringKey + let subtitleKey: LocalizedStringKey? + let value: String? + + var body: some View { + VStack(alignment: .leading, spacing: 2) { + Text(title) + if let subtitleKey { + Text(subtitleKey) + .font(.caption) + .foregroundStyle(.secondary) + .fixedSize(horizontal: false, vertical: true) + } + if let value { + Text(verbatim: value) + .font(.caption) + .foregroundStyle(.secondary) + .lineLimit(2) + .truncationMode(.middle) + .textSelection(.enabled) + } + } + } +} + +private struct MacAccountManagementSettingsPane: View { + @StateObject private var presenter = KotlinPresenter(presenter: AccountManagementPresenter()) + @State private var accounts: [AccountsStateAccountItem] = [] + @State private var isLoginSheetPresented = false + @State private var pendingLogoutAccountKey: MicroBlogKey? + @State private var pendingLogoutAccountName: String? + + var body: some View { + StateView(state: presenter.state.accounts) { data in + VStack { + Button { + isLoginSheetPresented = true + } label: { + Label { + Text("login_button") + } icon: { + Image(fontAwesome: .plus) + } + } + .frame(maxWidth: .infinity, alignment: .trailing) + .padding() + let currentAccounts = accounts.isEmpty ? data.cast(AccountsStateAccountItem.self) : accounts + if currentAccounts.isEmpty { + MacSettingsEmptyState( + pageTitle: "account_management_title", + pageSubtitle: "account_management_description", + title: "macos_account_unavailable", + systemImage: "person.crop.circle.badge.exclamationmark", + description: "macos_account_add" + ).onTapGesture { + isLoginSheetPresented = true + } + } else { + MacSettingsForm( + title: "account_management_title", + subtitle: "account_management_description" + ) { + Section { + ForEach(Array(currentAccounts.enumerated()), id: \.element.account.accountKey) { index, item in + accountRow(item: item, index: index, count: currentAccounts.count) + } + } + } + } + } + } loadingContent: { + MacSettingsLoadingState(title: "account_management_title", subtitle: "account_management_description") + } + .onSuccessOf(of: presenter.state.accounts) { data in + accounts = data.cast(AccountsStateAccountItem.self) + } + .sheet(isPresented: $isLoginSheetPresented) { + NavigationStack { + ServiceSelectionScreen { + isLoginSheetPresented = false + } + .navigationTitle("login_button") + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button { + isLoginSheetPresented = false + } label: { + Label { + Text("cancel_button") + } icon: { + Image(fontAwesome: .xmark) + } + } + } + } + } + .frame(width: 420, height: 540) + } + .alert("logout_title", isPresented: Binding(get: { + pendingLogoutAccountKey != nil + }, set: { value in + if !value { + clearPendingLogout() + } + })) { + Button("cancel_button", role: .cancel) { + clearPendingLogout() + } + Button("delete_button", role: .destructive) { + confirmLogout() + } + } message: { + Text( + pendingLogoutAccountName.map { "Are you sure you want to remove \($0) from this device?" } ?? + "Are you sure you want to remove this account from this device?" + ) + } + } + + @ViewBuilder + private func accountRow(item: AccountsStateAccountItem, index: Int, count: Int) -> some View { + StateView(state: item.profile) { user in + UserCompatView(data: user) { + HStack(spacing: 8) { + Image(systemName: activeAccountKey == user.key ? "checkmark.circle.fill" : "circle") + .foregroundStyle(activeAccountKey == user.key ? Color.accentColor : Color.secondary) + + MacAccountOrderButtons( + canMoveUp: index > 0, + canMoveDown: index < count - 1, + moveUp: { move(item: item, by: -1) }, + moveDown: { move(item: item, by: 1) } + ) + } + } + .contentShape(Rectangle()) + .onTapGesture { + presenter.state.setActiveAccount(accountKey: user.key) + } + .contextMenu { + accountMenu(item: item, accountName: user.handle.canonical) + } + } errorContent: { error in + UserErrorView(error: error) + .contextMenu { + accountMenu(item: item, accountName: item.account.accountKey.id) + } + } loadingContent: { + UserLoadingView() + } + } + + @ViewBuilder + private func accountMenu(item: AccountsStateAccountItem, accountName: String?) -> some View { + Button(role: .destructive) { + requestLogoutConfirmation( + accountKey: item.account.accountKey, + accountName: accountName + ) + } label: { + Label { + Text("logout_title") + } icon: { + Image(fontAwesome: .trash) + } + } + } + + private var activeAccountKey: MicroBlogKey? { + if case .success(let active) = onEnum(of: presenter.state.activeAccount) { + active.data.accountKey + } else { + nil + } + } + + private func move(item: AccountsStateAccountItem, by offset: Int) { + guard let currentIndex = accounts.firstIndex(where: { $0.account.accountKey == item.account.accountKey }) else { + return + } + let targetIndex = currentIndex + offset + guard accounts.indices.contains(targetIndex) else { + return + } + accounts.move( + fromOffsets: IndexSet(integer: currentIndex), + toOffset: offset > 0 ? targetIndex + 1 : targetIndex + ) + presenter.state.setOrder(value: accounts.map { $0.account.accountKey }) + } + + private func requestLogoutConfirmation(accountKey: MicroBlogKey, accountName: String?) { + pendingLogoutAccountKey = accountKey + pendingLogoutAccountName = accountName + } + + private func confirmLogout() { + guard let accountKey = pendingLogoutAccountKey else { + return + } + accounts.removeAll { item in + item.account.accountKey == accountKey + } + presenter.state.logout(accountKey: accountKey) + presenter.state.setOrder(value: accounts.map { $0.account.accountKey }) + clearPendingLogout() + } + + private func clearPendingLogout() { + pendingLogoutAccountKey = nil + pendingLogoutAccountName = nil + } +} + +private struct MacAccountOrderButtons: View { + let canMoveUp: Bool + let canMoveDown: Bool + let moveUp: () -> Void + let moveDown: () -> Void + + var body: some View { + HStack(spacing: 4) { + Button(action: moveUp) { + Label { + Text("macos_action_move_up") + } icon: { + Image(systemName: "chevron.up") + } + } + .buttonStyle(.borderless) + .disabled(!canMoveUp) + .help(Text("macos_action_move_up")) + + Button(action: moveDown) { + Label { + Text("macos_action_move_down") + } icon: { + Image(systemName: "chevron.down") + } + } + .buttonStyle(.borderless) + .disabled(!canMoveDown) + .help(Text("macos_action_move_down")) + } + .font(.caption) + } +} + +private struct MacAppearanceSettingsPane: View { + @StateObject private var presenter = KotlinPresenter(presenter: SettingsPresenter()) + @Environment(\.globalAppearance) private var globalAppearance + @Environment(\.timelineAppearance) private var timelineAppearance + + var body: some View { + MacSettingsForm( + title: "macos_settings_section_appearance", + subtitle: "appearance_description" + ) { + Section("appearance_theme_group_title") { + Picker(selection: Binding(get: { + globalAppearance.theme + }, set: { newValue in + presenter.state.updateTheme(value: newValue) + })) { + Text("appearance_theme_system").tag(Theme.system) + Text("appearance_theme_light").tag(Theme.light) + Text("appearance_theme_dark").tag(Theme.dark) + } label: { + MacSettingLabel("appearance_theme", subtitle: "appearance_theme_description") + } + + Picker(selection: Binding(get: { + timelineAppearance.avatarShape + }, set: { newValue in + presenter.state.updateAvatarShape(value: newValue) + })) { + Text("appearance_avatar_shape_circle").tag(AvatarShape.circle) + Text("appearance_avatar_shape_square").tag(AvatarShape.square) + } label: { + MacSettingLabel("appearance_avatar_shape", subtitle: "appearance_avatar_shape_description") + } + } + + Section("appearance_layout_group_title") { + Picker(selection: Binding(get: { + timelineAppearance.timelineDisplayMode + }, set: { newValue in + presenter.state.updateTimelineDisplayMode(value: newValue) + })) { + Text("appearance_timeline_display_mode_card").tag(TimelineDisplayMode.card) + Text("appearance_timeline_display_mode_plain").tag(TimelineDisplayMode.plain) + Text("appearance_timeline_display_mode_gallery").tag(TimelineDisplayMode.gallery) + } label: { + MacSettingLabel( + "appearance_timeline_display_mode", + subtitle: "appearance_timeline_display_mode_description" + ) + } + + Toggle(isOn: Binding(get: { + globalAppearance.showBottomBarLabels + }, set: { newValue in + presenter.state.updateShowBottomBarLabels(value: newValue) + })) { + MacSettingLabel( + "appearance_show_bottom_bar_labels", + subtitle: "appearance_show_bottom_bar_labels_description" + ) + } + + Toggle(isOn: Binding(get: { + globalAppearance.deckMode + }, set: { newValue in + presenter.state.updateDeckMode(value: newValue) + })) { + MacSettingLabel("appearance_deck_mode", subtitle: "appearance_deck_mode_description") + } + + Toggle(isOn: Binding(get: { + timelineAppearance.fullWidthPost + }, set: { newValue in + presenter.state.updateFullWidthPost(value: newValue) + })) { + MacSettingLabel("appearance_fullWidthPost", subtitle: "appearance_fullWidthPost_description") + } + + Picker(selection: Binding(get: { + timelineAppearance.postActionStyle + }, set: { newValue in + presenter.state.updatePostActionStyle(value: newValue) + })) { + Text("appearance_post_action_style_hidden").tag(PostActionStyle.hidden) + Text("appearance_post_action_style_left_aligned").tag(PostActionStyle.leftAligned) + Text("appearance_post_action_style_right_aligned").tag(PostActionStyle.rightAligned) + Text("appearance_post_action_style_stretch").tag(PostActionStyle.stretch) + } label: { + MacSettingLabel( + "appearance_post_action_style", + subtitle: "appearance_post_action_style_description" + ) + } + + if timelineAppearance.postActionStyle != .hidden { + Toggle(isOn: Binding(get: { + timelineAppearance.showNumbers + }, set: { newValue in + presenter.state.updateShowNumbers(value: newValue) + })) { + MacSettingLabel("appearance_show_numbers", subtitle: "appearance_show_numbers_description") + } + } + } + + Section("appearance_display_group_title") { + Toggle(isOn: Binding(get: { + timelineAppearance.absoluteTimestamp + }, set: { newValue in + presenter.state.updateAbsoluteTimestamp(value: newValue) + })) { + MacSettingLabel( + "appearance_absolute_timestamp", + subtitle: "appearance_absolute_timestamp_description" + ) + } + + Toggle(isOn: Binding(get: { + timelineAppearance.showPlatformLogo + }, set: { newValue in + presenter.state.updateShowPlatformLogo(value: newValue) + })) { + MacSettingLabel( + "appearance_show_platform_logo", + subtitle: "appearance_show_platform_logo_description" + ) + } + + Toggle(isOn: Binding(get: { + timelineAppearance.showLinkPreview + }, set: { newValue in + presenter.state.updateShowLinkPreview(value: newValue) + })) { + MacSettingLabel( + "appearance_show_link_preview", + subtitle: "appearance_show_link_preview_description" + ) + } + + if timelineAppearance.showLinkPreview { + Toggle(isOn: Binding(get: { + timelineAppearance.compatLinkPreview + }, set: { newValue in + presenter.state.updateCompatLinkPreview(value: newValue) + })) { + MacSettingLabel( + "appearance_compat_link_preview", + subtitle: "appearance_compat_link_preview_description" + ) + } + } + + Toggle(isOn: Binding(get: { + globalAppearance.inAppBrowser + }, set: { newValue in + presenter.state.updateInAppBrowser(value: newValue) + })) { + MacSettingLabel("appearance_in_app_browser", subtitle: "appearance_in_app_browser_description") + } + } + + Section("appearance_media_group_title") { + Toggle(isOn: Binding(get: { + timelineAppearance.showMedia + }, set: { newValue in + presenter.state.updateShowMedia(value: newValue) + })) { + MacSettingLabel("appearance_show_media", subtitle: "appearance_show_media_description") + } + + if timelineAppearance.showMedia { + Toggle(isOn: Binding(get: { + timelineAppearance.expandMediaSize + }, set: { newValue in + presenter.state.updateExpandMediaSize(value: newValue) + })) { + MacSettingLabel( + "appearance_expand_media_size", + subtitle: "appearance_expand_media_size_description" + ) + } + + Toggle(isOn: Binding(get: { + timelineAppearance.showSensitiveContent + }, set: { newValue in + presenter.state.updateShowSensitiveContent(value: newValue) + })) { + MacSettingLabel( + "appearance_show_sensitive_content", + subtitle: "appearance_show_sensitive_content_description" + ) + } + + Toggle(isOn: Binding(get: { + timelineAppearance.expandContentWarning + }, set: { newValue in + presenter.state.updateExpandContentWarning(value: newValue) + })) { + MacSettingLabel( + "appearance_expand_content_warning", + subtitle: "appearance_expand_content_warning_description" + ) + } + + Picker(selection: Binding(get: { + timelineAppearance.videoAutoplay + }, set: { newValue in + presenter.state.updateVideoAutoplay(value: newValue) + })) { + Text("appearance_video_autoplay_never").tag(VideoAutoplay.never) + Text("appearance_video_autoplay_wifi").tag(VideoAutoplay.wifi) + Text("appearance_video_autoplay_always").tag(VideoAutoplay.always) + } label: { + MacSettingLabel("appearance_video_autoplay", subtitle: "appearance_video_autoplay_description") + } + } + } + } + } +} + +private struct MacLocalFilterSettingsPane: View { + @StateObject private var presenter = KotlinPresenter(presenter: LocalFilterPresenter()) + @State private var selectedFilter: UiKeywordFilter? + @State private var showingEditor = false + + var body: some View { + StateView(state: presenter.state.items) { filters in + VStack { + Button { + selectedFilter = nil + showingEditor = true + } label: { + Label { + Text("local_filter_edit_title") + } icon: { + Image(fontAwesome: .plus) + } + } + .frame(maxWidth: .infinity, alignment: .trailing) + .padding() + let list = filters.cast(UiKeywordFilter.self) + if list.isEmpty { + MacSettingsEmptyState( + pageTitle: "local_filter_title", + pageSubtitle: "local_filter_description", + title: "list_empty_title", + systemImage: "line.3.horizontal.decrease.circle" + ) + } else { + MacSettingsForm( + title: "local_filter_title", + subtitle: "local_filter_description" + ) { + Section { + ForEach(list, id: \.keyword) { item in + MacLocalFilterRow(item: item) + .contextMenu { + Button { + selectedFilter = item + showingEditor = true + } label: { + Label { + Text("local_filter_edit") + } icon: { + Image(fontAwesome: .pen) + } + } + Button(role: .destructive) { + presenter.state.delete(keyword: item.keyword) + } label: { + Label { + Text("local_filter_delete") + } icon: { + Image(fontAwesome: .trash) + } + } + } + } + } + } + } + } + } loadingContent: { + MacSettingsLoadingState(title: "local_filter_title", subtitle: "local_filter_description") + } + .sheet(isPresented: $showingEditor, onDismiss: { + selectedFilter = nil + }) { + NavigationStack { + MacLocalFilterEditSheet(filter: selectedFilter) { keyword, forTimeline, forNotification, forSearch, isRegex in + let item = UiKeywordFilter( + keyword: keyword, + forTimeline: forTimeline, + forNotification: forNotification, + forSearch: forSearch, + expiredAt: nil, + isRegex: isRegex + ) + if let selectedFilter { + if selectedFilter.keyword == keyword { + presenter.state.update(item: item) + } else { + presenter.state.delete(keyword: selectedFilter.keyword) + presenter.state.add(item: item) + } + } else { + presenter.state.add(item: item) + } + } + } + .frame(width: 420, height: 340) + } + } +} + +private struct MacLocalFilterRow: View { + let item: UiKeywordFilter + + var body: some View { + VStack(alignment: .leading, spacing: 4) { + Text(item.keyword) + HStack(spacing: 8) { + if item.forTimeline { + Text("local_filter_timeline") + } + if item.forNotification { + Text("local_filter_notification") + } + if item.forSearch { + Text("local_filter_search") + } + if item.isRegex { + Text("local_filter_regex") + } + } + .font(.caption) + .foregroundStyle(.secondary) + } + .frame(maxWidth: .infinity, alignment: .leading) + } +} + +private struct MacLocalFilterEditSheet: View { + let onConfirm: (String, Bool, Bool, Bool, Bool) -> Void + @Environment(\.dismiss) private var dismiss + @State private var keyword = "" + @State private var forTimeline = true + @State private var forNotification = true + @State private var forSearch = true + @State private var isRegex = false + + var body: some View { + Form { + Section("local_filter_keyword_header") { + TextField("local_filter_keyword_placeholder", text: $keyword) + } + + Section("local_filter_scope_header") { + Toggle("local_filter_timeline", isOn: $forTimeline) + Toggle("local_filter_notification", isOn: $forNotification) + Toggle("local_filter_search", isOn: $forSearch) + Toggle("local_filter_regex", isOn: $isRegex) + } + } + .formStyle(.grouped) + .navigationTitle("local_filter_edit_title") + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button { + dismiss() + } label: { + Label { + Text("cancel_button") + } icon: { + Image(fontAwesome: .xmark) + } + } + } + ToolbarItem(placement: .confirmationAction) { + Button { + onConfirm(keyword, forTimeline, forNotification, forSearch, isRegex) + dismiss() + } label: { + Label { + Text("ok_button") + } icon: { + Image(fontAwesome: .check) + } + } + .disabled(keyword.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) + } + } + } + + init( + filter: UiKeywordFilter?, + onConfirm: @escaping (String, Bool, Bool, Bool, Bool) -> Void + ) { + self.onConfirm = onConfirm + if let filter { + _keyword = .init(initialValue: filter.keyword) + _forTimeline = .init(initialValue: filter.forTimeline) + _forNotification = .init(initialValue: filter.forNotification) + _forSearch = .init(initialValue: filter.forSearch) + _isRegex = .init(initialValue: filter.isRegex) + } + } +} + +private struct MacStorageSettingsPane: View { + @StateObject private var presenter: KotlinPresenter + @State private var showDatabaseClearAlert = false + @State private var showImageClearAlert = false + @State private var showFileExporter = false + @State private var showFileImporter = false + @State private var showImportConfirmation = false + @State private var pendingImportJson: String? + @State private var jsonFile = MacJSONFile(text: "") + @State private var notice: MacStorageNotice? + @State private var isClearingImageCache = false + @State private var isClearingDatabaseCache = false + @State private var showingAppLog = false + + init() { + _presenter = StateObject(wrappedValue: KotlinPresenter(presenter: StoragePresenter())) + } + + var body: some View { + MacSettingsForm( + title: "storage_title", + subtitle: "storage_description" + ) { + Section { + MacSettingActionRow( + "storage_clear_image_cache", + subtitle: "storage_clear_image_cache_desc", + buttonTitle: "macos_action_clear", + icon: .trash, + role: .destructive + ) { + guard !isClearingStorage else { + return + } + showImageClearAlert = true + } + + MacSettingActionRow( + "storage_clear_database_cache", + value: "\(presenter.state.userCount) users, \(presenter.state.statusCount) posts", + buttonTitle: "macos_action_clear", + icon: .trash, + role: .destructive + ) { + guard !isClearingStorage else { + return + } + showDatabaseClearAlert = true + } + } + + Section { + MacSettingActionRow( + "storage_view_app_log", + subtitle: "storage_view_app_log_desc", + buttonTitle: "macos_action_open", + icon: .envelope + ) { + showingAppLog = true + } + + MacSettingActionRow( + "settings_storage_export_data", + subtitle: "settings_storage_export_data_desc", + buttonTitle: "macos_action_export", + icon: .fileExport + ) { + exportData() + } + + MacSettingActionRow( + "settings_storage_import_data", + subtitle: "settings_storage_import_data_desc", + buttonTitle: "macos_action_import", + icon: .fileImport + ) { + guard !isClearingStorage else { + return + } + showFileImporter = true + } + } + } + .disabled(isClearingStorage) + .overlay { + if isClearingStorage { + ZStack { + Color.black.opacity(0.12) + ProgressView() + .controlSize(.large) + } + } + } + .alert("storage_clear_image_cache_confirm", isPresented: $showImageClearAlert) { + Button("cancel_button", role: .cancel) {} + Button("ok_button", role: .destructive) { + clearImageCache() + } + } + .alert("storage_clear_database_cache_confirm", isPresented: $showDatabaseClearAlert) { + Button("cancel_button", role: .cancel) {} + Button("ok_button", role: .destructive) { + clearDatabaseCache() + } + } + .alert("import_confirmation_title", isPresented: $showImportConfirmation) { + Button("cancel_button", role: .cancel) { + pendingImportJson = nil + } + Button("ok_button") { + importData() + } + } message: { + Text("import_confirmation_message") + } + .alert(item: $notice) { notice in + Alert( + title: Text(notice.title), + message: Text(notice.message), + dismissButton: .default(Text("OK")) + ) + } + .fileExporter( + isPresented: $showFileExporter, + document: jsonFile, + contentType: .json, + defaultFilename: "flare_data_export" + ) { result in + switch result { + case .success: + notice = MacStorageNotice(title: "save_completed", message: "") + case .failure(let error): + notice = MacStorageNotice(title: "save_error", message: error.localizedDescription) + } + } + .fileImporter(isPresented: $showFileImporter, allowedContentTypes: [.json]) { result in + switch result { + case .success(let url): + guard url.startAccessingSecurityScopedResource() else { + notice = MacStorageNotice(title: "import_error", message: "") + return + } + defer { + url.stopAccessingSecurityScopedResource() + } + + do { + let data = try Data(contentsOf: url) + pendingImportJson = String(data: data, encoding: .utf8) + showImportConfirmation = pendingImportJson != nil + } catch { + notice = MacStorageNotice(title: "import_error", message: error.localizedDescription) + } + case .failure(let error): + notice = MacStorageNotice(title: "import_error", message: error.localizedDescription) + } + } + .sheet(isPresented: $showingAppLog) { + NavigationStack { + MacAppLogSettingsPane() + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button { + showingAppLog = false + } label: { + Label { + Text("cancel_button") + } icon: { + Image(fontAwesome: .xmark) + } + } + } + } + } + .frame(width: 620, height: 560) + } + } + + private var isClearingStorage: Bool { + isClearingImageCache || isClearingDatabaseCache + } + + private func clearImageCache() { + isClearingImageCache = true + URLCache.shared.removeAllCachedResponses() + KingfisherManager.shared.cache.clearMemoryCache() + let websiteDataTypes: Set = [ + WKWebsiteDataTypeMemoryCache, + WKWebsiteDataTypeDiskCache + ] + WKWebsiteDataStore.default().removeData( + ofTypes: websiteDataTypes, + modifiedSince: .distantPast + ) {} + KingfisherManager.shared.cache.clearDiskCache { + Task { @MainActor in + isClearingImageCache = false + } + } + } + + private func clearDatabaseCache() { + isClearingDatabaseCache = true + presenter.state.clearCache() + Task { @MainActor in + try? await Task.sleep(nanoseconds: 300_000_000) + isClearingDatabaseCache = false + } + } + + private func exportData() { + guard !isClearingStorage else { + return + } + Task.detached { + do { + let json = try await ExportDataPresenter().export() + await MainActor.run { + jsonFile = MacJSONFile(text: json) + showFileExporter = true + } + } catch { + let message = error.localizedDescription + await MainActor.run { + notice = MacStorageNotice(title: "export_error", message: message) + } + } + } + } + + private func importData() { + guard let json = pendingImportJson else { + return + } + pendingImportJson = nil + Task.detached { + do { + let importPresenter = ImportDataPresenter(jsonContent: json) + try await importPresenter.models.value.import() + await MainActor.run { + notice = MacStorageNotice(title: "import_completed", message: "") + } + } catch { + let message = error.localizedDescription + await MainActor.run { + notice = MacStorageNotice(title: "import_error", message: message) + } + } + } + } +} + +private struct MacStorageNotice: Identifiable { + let id = UUID() + let title: LocalizedStringKey + let message: String +} + +private struct MacJSONFile: FileDocument { + static let readableContentTypes = [UTType.json] + var text = "" + + init(text: String = "") { + self.text = text + } + + init(configuration: ReadConfiguration) throws { + if let data = configuration.file.regularFileContents { + text = String(decoding: data, as: UTF8.self) + } + } + + func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper { + FileWrapper(regularFileWithContents: Data(text.utf8)) + } +} + +private struct MacAiConfigSettingsPane: View { + @StateObject private var presenter = KotlinPresenter(presenter: AiConfigPresenter()) + @State private var editingField: MacAiEditableField? + @State private var editingText = "" + + var body: some View { + MacSettingsForm( + title: "ai_config_title", + subtitle: "ai_config_description" + ) { + Section { + Picker(selection: Binding(get: { + presenter.state.aiType + }, set: { type in + presenter.state.selectType(type: type) + })) { + ForEach(presenter.state.supportedTypes, id: \.name) { type in + Text(aiTypeOptionTitle(option: type)).tag(type) + } + } label: { + MacSettingLabel("AI Type", subtitle: "Select AI provider") + } + + if presenter.state.aiType == .openAi { + editableButton(field: .serverUrl, value: presenter.state.openAIServerUrl) + editableButton(field: .apiKey, value: presenter.state.openAIApiKey) + + Picker(selection: Binding(get: { + presenter.state.openAIReasoningEffort + }, set: { effort in + presenter.state.setOpenAIReasoningEffort(value: effort) + })) { + ForEach(presenter.state.supportedOpenAIReasoningEfforts, id: \.name) { effort in + Text(reasoningEffortTitle(option: effort)).tag(effort) + } + } label: { + MacSettingLabel( + "Reasoning Effort", + subtitle: "Choose how much effort the model spends on reasoning. Default uses the provider's default behavior." + ) + } + + editableButton(field: .extraBody, value: presenter.state.openAIExtraBody) + + if shouldShowManualModelInput { + editableButton(field: .model, value: presenter.state.openAIModel) + } else { + let selectedModel = presenter.state.openAIModel + Picker(selection: Binding(get: { + selectedModel + }, set: { model in + if !model.hasPrefix("__meta__") { + presenter.state.setOpenAIModel(value: model) + } + })) { + switch onEnum(of: presenter.state.openAIModels) { + case .loading: + if !selectedModel.isEmpty { + Text(selectedModel).tag(selectedModel) + } + Text("Loading models...").tag("__meta__loading") + case .success(let data): + let models = (data.data as NSArray).cast(NSString.self).map(String.init) + ForEach(models, id: \.self) { model in + Text(model).tag(model) + } + case .error: + EmptyView() + } + } label: { + MacSettingLabel("Model", subtitle: "AI model used for translation and summary") + } + } + } + } + + Section { + Toggle(isOn: Binding(get: { + presenter.state.aiAgent + }, set: { newValue in + presenter.state.setAIAgent(value: newValue) + })) { + MacSettingLabel("ai_config_post_insight", subtitle: "ai_config_post_insight_description") + } + } + + Section { + Toggle(isOn: Binding(get: { + presenter.state.aiTldr + }, set: { newValue in + presenter.state.setAITldr(value: newValue) + })) { + MacSettingLabel("ai_config_summarize", subtitle: "Summarize long text with AI") + } + + if presenter.state.aiTldr { + editableButton(field: .tldrPrompt, value: presenter.state.tldrPrompt) + } + } + } + .animation(.easeInOut(duration: 0.2), value: presenter.state.aiType == .openAi) + .animation(.easeInOut(duration: 0.2), value: presenter.state.aiTldr) + .sheet(item: $editingField) { field in + MacTextEditSheet( + title: field.title, + text: $editingText, + isMultiline: field.isMultiline, + placeholder: field.placeholder, + footer: field == .serverUrl ? serverUrlHint : (field == .extraBody ? extraBodyHint : nil), + suggestions: field == .serverUrl ? filteredServerSuggestions(query: editingText) : [], + onSelectSuggestion: { suggestion in + editingText = suggestion + }, + onCancel: { + editingField = nil + }, + onConfirm: { + applyEdit(field: field, value: editingText) + editingField = nil + } + ) + } + } + + private func editableButton(field: MacAiEditableField, value: String) -> some View { + MacSettingActionRow( + field.title, + value: field.displayValue(value), + buttonTitle: "macos_action_edit", + icon: .pen + ) { + editingText = value + editingField = field + } + } + + private func aiTypeOptionTitle(option: AiTypeOption) -> LocalizedStringResource { + switch option { + case .onDevice: + "On Device" + case .openAi: + "AI-compatible API" + } + } + + private func reasoningEffortTitle(option: AiReasoningEffortOption) -> LocalizedStringResource { + switch option { + case .default: + "Default" + case .low: + "Low" + case .medium: + "Medium" + case .high: + "High" + } + } + + private var shouldShowManualModelInput: Bool { + switch onEnum(of: presenter.state.openAIModels) { + case .loading: + false + case .error: + true + case .success(let data): + (data.data as NSArray).cast(NSString.self).isEmpty + } + } + + private var serverUrlHint: String { + "Server URL must end with '/' and support the AI-compatible v1/chat/completions API." + } + + private var extraBodyHint: String { + "{\"thinking\": {\"type\": \"enabled\"}}" + } + + private var serverSuggestions: [String] { + (presenter.state.serverSuggestions as NSArray).cast(NSString.self).map(String.init) + } + + private func filteredServerSuggestions(query: String) -> [String] { + if query.isEmpty { + serverSuggestions + } else { + serverSuggestions.filter { $0.localizedCaseInsensitiveContains(query) } + } + } + + private func applyEdit(field: MacAiEditableField, value: String) { + switch field { + case .serverUrl: + presenter.state.setOpenAIServerUrl(value: value) + case .apiKey: + presenter.state.setOpenAIApiKey(value: value) + case .extraBody: + presenter.state.setOpenAIExtraBody(value: value) + case .model: + presenter.state.setOpenAIModel(value: value) + case .tldrPrompt: + presenter.state.setTldrPrompt(value: value) + } + } +} + +private enum MacAiEditableField: String, Identifiable { + case serverUrl + case apiKey + case extraBody + case model + case tldrPrompt + + var id: String { rawValue } + + var title: LocalizedStringKey { + switch self { + case .serverUrl: + "Server URL" + case .apiKey: + "API Key" + case .extraBody: + "Extra Body" + case .model: + "Manual Model" + case .tldrPrompt: + "Summary Prompt" + } + } + + var placeholder: String { + switch self { + case .serverUrl: + "https://api.example.com/v1/" + case .apiKey: + "sk-..." + case .extraBody: + "{\"thinking\": {\"type\": \"enabled\"}}" + case .model: + "model-name" + case .tldrPrompt: + "" + } + } + + var isMultiline: Bool { + self == .extraBody || self == .tldrPrompt + } + + func displayValue(_ value: String) -> String { + if value.isEmpty { + switch self { + case .model: + String(localized: "Select model") + default: + String(localized: "Not set") + } + } else { + value + } + } +} + +private struct MacTranslationConfigSettingsPane: View { + @StateObject private var presenter = KotlinPresenter(presenter: AiConfigPresenter()) + @StateObject private var aiTranslationTestPresenter = KotlinPresenter(presenter: AiTranslationTestPresenter()) + @State private var editingField: MacTranslationEditableField? + @State private var editingText = "" + @State private var showExcludedLanguagesPicker = false + @State private var pendingExcludedLanguages: Set = [] + @State private var excludedLanguagesQuery = "" + + var body: some View { + MacSettingsForm( + title: "settings_translation_title", + subtitle: "settings_translation_description" + ) { + Section { + Picker(selection: Binding(get: { + presenter.state.translateProvider + }, set: { provider in + presenter.state.selectTranslateProvider(type: provider) + })) { + ForEach(presenter.state.supportedTranslateProviders, id: \.name) { provider in + Text(translateProviderOptionTitle(option: provider)).tag(provider) + } + } label: { + MacSettingLabel("Translation Provider", subtitle: "Choose which service handles translation") + } + + Toggle(isOn: Binding(get: { + presenter.state.preTranslate + }, set: { newValue in + presenter.state.setPreTranslate(value: newValue) + })) { + MacSettingLabel("ai_config_pre_translate", subtitle: "ai_config_pre_translate_description") + } + + if presenter.state.preTranslate { + MacSettingActionRow( + "Auto-translate excluded languages", + value: displayExcludedLanguages, + buttonTitle: "macos_action_edit", + icon: .pen + ) { + pendingExcludedLanguages = Set(excludedLanguages) + excludedLanguagesQuery = "" + showExcludedLanguagesPicker = true + } + } + + providerSpecificControls + } + } + .animation(.easeInOut(duration: 0.2), value: presenter.state.translateProvider.name) + .animation(.easeInOut(duration: 0.2), value: presenter.state.preTranslate) + .sheet(item: $editingField) { field in + MacTextEditSheet( + title: field.title, + text: $editingText, + isMultiline: true, + placeholder: "", + footer: nil, + suggestions: [], + onSelectSuggestion: { _ in }, + onCancel: { + editingField = nil + }, + onConfirm: { + applyTranslationEdit(field: field, value: editingText) + editingField = nil + } + ) + } + .sheet(isPresented: $showExcludedLanguagesPicker) { + NavigationStack { + List(selection: $pendingExcludedLanguages) { + ForEach(filteredLanguageOptions) { option in + VStack(alignment: .leading, spacing: 2) { + Text(option.title) + if option.title != option.tag { + Text(option.tag) + .font(.caption) + .foregroundStyle(.secondary) + } + } + .tag(option.tag) + } + } + .searchable(text: $excludedLanguagesQuery, prompt: "Search language") + .navigationTitle("Auto-translate excluded languages") + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button { + showExcludedLanguagesPicker = false + } label: { + Label { + Text("cancel_button") + } icon: { + Image(fontAwesome: .xmark) + } + } + } + ToolbarItem(placement: .confirmationAction) { + Button { + presenter.state.setAutoTranslateExcludedLanguages( + value: languageOptions + .map(\.tag) + .filter { pendingExcludedLanguages.contains($0) } + ) + showExcludedLanguagesPicker = false + } label: { + Label { + Text("ok_button") + } icon: { + Image(fontAwesome: .check) + } + } + } + } + } + .frame(width: 460, height: 560) + } + } + + @ViewBuilder + private var providerSpecificControls: some View { + switch presenter.state.translateProvider { + case .ai: + editableButton(field: .translatePrompt, value: presenter.state.translatePrompt) + + VStack(alignment: .leading, spacing: 8) { + Text("AI translation test") + .font(.headline) + Text("Run a short rich-text sample through the current AI translation setup.") + .font(.caption) + .foregroundStyle(.secondary) + Text("Sample text") + .font(.caption) + .foregroundStyle(.secondary) + RichText(text: aiTranslationTestPresenter.state.sampleText) + MacSettingActionRow( + "Test translation", + subtitle: "settings_translation_test_description", + buttonTitle: "macos_action_run", + icon: .play + ) { + aiTranslationTestPresenter.state.runTest() + } + if aiTranslationTestPresenter.state.isLoading { + ProgressView() + } + if let errorMessage = aiTranslationTestPresenter.state.errorMessage { + Text(errorMessage) + .foregroundStyle(.red) + .font(.footnote) + } + if let translatedText = aiTranslationTestPresenter.state.translatedText { + Text("Translated text") + .font(.caption) + .foregroundStyle(.secondary) + RichText(text: translatedText) + } + } + .padding(.vertical, 4) + + case .deepL: + editableButton(field: .deepLApiKey, value: presenter.state.deepLApiKey) + Toggle(isOn: Binding(get: { + presenter.state.deepLUsePro + }, set: { newValue in + presenter.state.setDeepLUsePro(value: newValue) + })) { + MacSettingLabel("DeepL Pro Endpoint", subtitle: "Use api.deepl.com instead of the free endpoint") + } + + case .googleCloud: + editableButton(field: .googleCloudApiKey, value: presenter.state.googleCloudApiKey) + + case .libreTranslate: + editableButton(field: .libreTranslateBaseUrl, value: presenter.state.libreTranslateBaseUrl) + editableButton(field: .libreTranslateApiKey, value: presenter.state.libreTranslateApiKey) + + case .googleWeb: + EmptyView() + } + } + + private func editableButton(field: MacTranslationEditableField, value: String) -> some View { + MacSettingActionRow( + field.title, + value: value.isEmpty ? String(localized: "Not set") : value, + buttonTitle: "macos_action_edit", + icon: .pen + ) { + editingText = value + editingField = field + } + } + + private func translateProviderOptionTitle(option: TranslateProviderOption) -> LocalizedStringResource { + switch option { + case .ai: + "AI" + case .googleWeb: + "Google Translate (Web)" + case .deepL: + "DeepL" + case .googleCloud: + "Google Cloud Translate" + case .libreTranslate: + "LibreTranslate" + } + } + + private var excludedLanguages: [String] { + (presenter.state.autoTranslateExcludedLanguages as NSArray).cast(NSString.self).map(String.init) + } + + private var languageOptions: [MacLanguageOption] { + let current = Locale.current + let baseOptions = Locale.LanguageCode.isoLanguageCodes.map { code in + let tag = code.identifier + return MacLanguageOption( + tag: tag, + title: current.localizedString(forLanguageCode: tag) ?? tag + ) + } + let specialOptions = [ + MacLanguageOption( + tag: "zh-CN", + title: current.localizedString(forIdentifier: "zh-Hans") ?? "Chinese (Simplified)" + ), + MacLanguageOption( + tag: "zh-TW", + title: current.localizedString(forIdentifier: "zh-Hant") ?? "Chinese (Traditional)" + ) + ] + let knownTags = Set((specialOptions + baseOptions).map(\.tag)) + let customOptions = excludedLanguages + .filter { !knownTags.contains($0) } + .map { MacLanguageOption(tag: $0, title: $0) } + return (specialOptions + baseOptions + customOptions) + .reduce(into: [String: MacLanguageOption]()) { result, option in + result[option.tag] = result[option.tag] ?? option + } + .values + .sorted { $0.title.localizedCaseInsensitiveCompare($1.title) == .orderedAscending } + } + + private var displayExcludedLanguages: String { + if excludedLanguages.isEmpty { + return String(localized: "Not set") + } + let titles = Dictionary(uniqueKeysWithValues: languageOptions.map { ($0.tag, $0.title) }) + return excludedLanguages.map { titles[$0] ?? $0 }.joined(separator: ", ") + } + + private var filteredLanguageOptions: [MacLanguageOption] { + if excludedLanguagesQuery.isEmpty { + languageOptions + } else { + languageOptions.filter { option in + option.title.localizedCaseInsensitiveContains(excludedLanguagesQuery) || + option.tag.localizedCaseInsensitiveContains(excludedLanguagesQuery) + } + } + } + + private func applyTranslationEdit(field: MacTranslationEditableField, value: String) { + switch field { + case .translatePrompt: + presenter.state.setTranslatePrompt(value: value) + case .deepLApiKey: + presenter.state.setDeepLApiKey(value: value) + case .googleCloudApiKey: + presenter.state.setGoogleCloudApiKey(value: value) + case .libreTranslateBaseUrl: + presenter.state.setLibreTranslateBaseUrl(value: value) + case .libreTranslateApiKey: + presenter.state.setLibreTranslateApiKey(value: value) + } + } +} + +private enum MacTranslationEditableField: String, Identifiable { + case translatePrompt + case deepLApiKey + case googleCloudApiKey + case libreTranslateBaseUrl + case libreTranslateApiKey + + var id: String { rawValue } + + var title: LocalizedStringKey { + switch self { + case .translatePrompt: + "Translate Prompt" + case .deepLApiKey: + "DeepL API Key" + case .googleCloudApiKey: + "Google Cloud API Key" + case .libreTranslateBaseUrl: + "LibreTranslate Base URL" + case .libreTranslateApiKey: + "LibreTranslate API Key" + } + } +} + +private struct MacLanguageOption: Identifiable { + let tag: String + let title: String + + var id: String { tag } +} + +private struct MacTextEditSheet: View { + let title: LocalizedStringKey + @Binding var text: String + let isMultiline: Bool + let placeholder: String + let footer: String? + let suggestions: [String] + let onSelectSuggestion: (String) -> Void + let onCancel: () -> Void + let onConfirm: () -> Void + + var body: some View { + NavigationStack { + Form { + Section { + if isMultiline { + TextEditor(text: $text) + .frame(minHeight: 180) + .font(.body.monospaced()) + } else { + TextField(placeholder, text: $text) + .textFieldStyle(.roundedBorder) + } + } footer: { + if let footer { + Text(footer) + .font(.footnote) + } + } + + if !suggestions.isEmpty { + Section("Suggestions") { + ForEach(suggestions, id: \.self) { suggestion in + Button { + onSelectSuggestion(suggestion) + } label: { + Text(suggestion) + .font(.callout.monospaced()) + .lineLimit(1) + } + .buttonStyle(.plain) + } + } + } + } + .formStyle(.grouped) + .navigationTitle(title) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button(action: onCancel) { + Label { + Text("cancel_button") + } icon: { + Image(fontAwesome: .xmark) + } + } + } + ToolbarItem(placement: .confirmationAction) { + Button(action: onConfirm) { + Label { + Text("ok_button") + } icon: { + Image(fontAwesome: .check) + } + } + } + } + } + .frame(width: 520, height: 420) + } +} + +private struct MacAppLogSettingsPane: View { + @StateObject private var presenter = KotlinPresenter(presenter: DevModePresenter()) + @State private var selectedMessage: MacLogMessage? + @State private var exportedLogContent: String? + + var body: some View { + let messages = (presenter.state.messages as NSArray).cast(NSString.self).map(String.init) + + Form { + Section { + Toggle(isOn: Binding(get: { + presenter.state.enabled + }, set: { enabled in + presenter.state.setEnabled(value: enabled) + })) { + Text("app_log_network_toggle") + } + } + + if messages.isEmpty { + ContentUnavailableView("list_empty_title", systemImage: "doc.text.magnifyingglass") + } else { + Section("app_log") { + ForEach(messages, id: \.self) { message in + Text(message) + .lineLimit(3) + .onTapGesture { + selectedMessage = MacLogMessage(message: message) + } + } + } + } + } + .formStyle(.grouped) + .navigationTitle("app_log") + .toolbar { + ToolbarItem { + Button { + presenter.state.clear() + } label: { + Label { + Text("macos_action_clear") + } icon: { + Image(fontAwesome: .trash) + } + } + } + ToolbarItem { + Button { + exportedLogContent = presenter.state.printMessageToString() + } label: { + Label { + Text("macos_action_save") + } icon: { + Image(fontAwesome: .floppyDisk) + } + } + } + } + .fileExporter( + isPresented: Binding(get: { + exportedLogContent != nil + }, set: { newValue in + if !newValue { + exportedLogContent = nil + } + }), + document: TextDocument(text: exportedLogContent ?? ""), + defaultFilename: "flare_log.txt" + ) { _ in + exportedLogContent = nil + } + .sheet(item: $selectedMessage) { message in + NavigationStack { + ScrollView { + Text(message.message) + .padding() + .frame(maxWidth: .infinity, alignment: .leading) + .textSelection(.enabled) + } + .navigationTitle("app_log") + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button { + selectedMessage = nil + } label: { + Label { + Text("cancel_button") + } icon: { + Image(fontAwesome: .xmark) + } + } + } + } + } + .frame(width: 560, height: 420) + } + } +} + +private struct MacLogMessage: Identifiable { + let id = UUID() + let message: String +} + +private struct MacAboutSettingsPane: View { + @Environment(\.openURL) private var openURL + + var body: some View { + MacSettingsForm( + title: "about_title", + subtitle: "about_description" + ) { + Section { + VStack(spacing: 14) { + Image(nsImage: NSApp.applicationIconImage) + .resizable() + .aspectRatio(1, contentMode: .fit) + .frame(width: 96, height: 96) + .clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous)) + .shadow(color: .black.opacity(0.12), radius: 16, y: 8) + + VStack(spacing: 6) { + Text("Flare") + .font(.largeTitle.weight(.semibold)) + Text("settings_about_description") + .foregroundStyle(.secondary) + .multilineTextAlignment(.center) + if !version.isEmpty { + Text(verbatim: version) + .font(.footnote.monospacedDigit()) + .foregroundStyle(.tertiary) + } + } + } + .frame(maxWidth: .infinity) + .padding(.vertical, 16) + } + + Section { + ForEach(MacAboutLink.all) { item in + MacSettingActionRow( + item.titleKey, + value: item.subtitle, + buttonTitle: "macos_action_open", + icon: item.icon + ) { + openURL(item.url) + } + } + } + } + } + + private var version: String { + let versionName = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "" + let buildNumber = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "" + + switch (versionName.isEmpty, buildNumber.isEmpty) { + case (false, false): + return "\(versionName) (\(buildNumber))" + case (false, true): + return versionName + case (true, false): + return buildNumber + case (true, true): + return "" + } + } +} + +private struct MacAboutLink: Identifiable { + let id: String + let titleKey: LocalizedStringKey + let subtitle: String + let icon: FontAwesomeIcon + let url: URL + + static let all: [MacAboutLink] = [ + MacAboutLink( + id: "source-code", + titleKey: "settings_about_source_code", + subtitle: "https://github.com/DimensionDev/Flare", + icon: .github, + url: URL(string: "https://github.com/DimensionDev/Flare")! + ), + MacAboutLink( + id: "telegram", + titleKey: "settings_about_telegram", + subtitle: String(localized: "settings_about_telegram_description"), + icon: .telegram, + url: URL(string: "https://t.me/+VZ63fqNQXIA0MzVl")! + ), + MacAboutLink( + id: "discord", + titleKey: "settings_about_discord", + subtitle: String(localized: "settings_about_discord_description"), + icon: .discord, + url: URL(string: "https://discord.gg/De9NhXBryT")! + ), + MacAboutLink( + id: "localization", + titleKey: "settings_about_localization", + subtitle: String(localized: "settings_about_localization_description"), + icon: .language, + url: URL(string: "https://crowdin.com/project/flareapp")! + ), + MacAboutLink( + id: "privacy-policy", + titleKey: "settings_privacy_policy", + subtitle: "https://legal.mask.io/maskbook", + icon: .lock, + url: URL(string: "https://legal.mask.io/maskbook/")! + ) + ] +} diff --git a/appleApp/macos/UI/Screen/PlaceholderPanel.swift b/appleApp/macos/UI/Screen/PlaceholderPanel.swift new file mode 100644 index 0000000000..e5c42fd053 --- /dev/null +++ b/appleApp/macos/UI/Screen/PlaceholderPanel.swift @@ -0,0 +1,78 @@ +import AppleFontAwesome +import KotlinSharedUI +import SwiftUI + +struct PlaceholderPanel: View { + let destination: HomeTabsPresenterStateHomeTabs + + var body: some View { + ZStack { + Color(nsColor: .textBackgroundColor) + + VStack(spacing: 24) { + Image(fontAwesome: destination.macOSIcon) + .resizable() + .scaledToFit() + .frame(width: 48, height: 48) + .foregroundStyle(.secondary) + + VStack(spacing: 8) { + Text(destination.macOSTitle) + .font(.title.weight(.semibold)) + Text(destination.macOSPlaceholder) + .font(.body) + .foregroundStyle(.secondary) + .multilineTextAlignment(.center) + .frame(maxWidth: 420) + } + + if destination.macOSShowsTimelineSkeleton { + TimelineSkeleton() + .frame(maxWidth: 540) + } + } + .padding(32) + } + .toolbar { + ToolbarItem(placement: .primaryAction) { + Button { + } label: { + Image(fontAwesome: .arrowsRotate) + } + .help(LocalizedStrings.string("refresh", fallback: "Refresh")) + } + } + } +} + +private struct TimelineSkeleton: View { + var body: some View { + VStack(spacing: 12) { + ForEach(0..<3, id: \.self) { index in + HStack(alignment: .top, spacing: 12) { + Circle() + .fill(.quaternary) + .frame(width: 36, height: 36) + + VStack(alignment: .leading, spacing: 8) { + RoundedRectangle(cornerRadius: 4) + .fill(.quaternary) + .frame(width: index == 1 ? 180 : 140, height: 10) + RoundedRectangle(cornerRadius: 4) + .fill(.quaternary) + .frame(height: 10) + RoundedRectangle(cornerRadius: 4) + .fill(.quaternary) + .frame(width: index == 2 ? 260 : 320, height: 10) + } + } + .padding(14) + .background(.background, in: RoundedRectangle(cornerRadius: 8)) + .overlay { + RoundedRectangle(cornerRadius: 8) + .stroke(.quaternary) + } + } + } + } +} diff --git a/appleApp/macos/UI/Screen/ProfileScreen.swift b/appleApp/macos/UI/Screen/ProfileScreen.swift new file mode 100644 index 0000000000..becf4fb786 --- /dev/null +++ b/appleApp/macos/UI/Screen/ProfileScreen.swift @@ -0,0 +1,469 @@ +import AppleFontAwesome +import FlareAppleCore +import FlareAppleUI +@preconcurrency import KotlinSharedUI +import SwiftUI + +struct ProfileScreen: View { + let accountType: AccountType + let onFollowingClick: (MicroBlogKey) -> Void + let onFansClick: (MicroBlogKey) -> Void + let goBack: () -> Void + + @StateObject private var presenter: KotlinPresenter + @State private var selectedTab = 0 + + init( + accountType: AccountType, + userKey: MicroBlogKey?, + onFollowingClick: @escaping (MicroBlogKey) -> Void, + onFansClick: @escaping (MicroBlogKey) -> Void, + goBack: @escaping () -> Void = {} + ) { + self.accountType = accountType + self.onFollowingClick = onFollowingClick + self.onFansClick = onFansClick + self.goBack = goBack + _presenter = .init( + wrappedValue: .init( + presenter: ProfilePresenter(accountType: accountType, userKey: userKey) + ) + ) + } + + var body: some View { + compatBody + } + + var compatBody: some View { + ScrollView { + LazyVStack(spacing: 12, pinnedViews: [.sectionHeaders]) { + ProfileHeader( + user: presenter.state.userState, + relation: presenter.state.relationState, + followButtonState: presenter.state.followButtonState, + isMe: presenter.state.isMe, + onFollowClick: { user, followButtonState in + handleFollowAction(user: user, followButtonState: followButtonState) + }, + onFollowingClick: onFollowingClick, + onFansClick: onFansClick + ) + + Section { + StateView(state: presenter.state.tabs) { tabsArray in + let tabs = tabsArray.cast(ProfileState.Tab.self) + profileTabsContent(tabs: tabs) + } loadingContent: { + ProgressView() + .padding() + .frame(maxWidth: .infinity) + } + } header: { + profileTabPinnedHeader + } + } + .padding(.bottom, 16) + } + .toolbar { +// ToolbarItem(placement: .automatic) { +// switch onEnum(of: presenter.state.userState) { +// case .success(let user): +// RichText(text: user.data.name) +// case .loading, .error: +// Text(LocalizedStrings.string("profile_title", fallback: "Profile")) +// } +// } + if !presenter.state.actions.isEmpty { + ToolbarItemGroup(placement: .primaryAction) { + StatusActionsView( + data: presenter.state.actions, + useText: false, + allowSpacer: false + ) + } + } + } + } + + @ViewBuilder + private func profileTabsContent(tabs: [ProfileState.Tab]) -> some View { + Group { + if tabs.isEmpty { + ListEmptyView() + .padding() + } else { + profileTabContent(tab: tabs[clampedSelectedTabIndex(for: tabs)]) + } + } + .onAppear { + normalizeSelectedTab(tabCount: tabs.count) + } + .onChange(of: tabs.count) { _, count in + normalizeSelectedTab(tabCount: count) + } + } + + @ViewBuilder + private var profileTabPinnedHeader: some View { + if case .success(let tabState) = onEnum(of: presenter.state.tabs) { + let tabs = tabState.data.cast(ProfileState.Tab.self) + if tabs.count > 1 { + ProfileTabPicker(tabs: tabs, selectedTab: $selectedTab) + .pickerStyle(.segmented) + .padding(.horizontal) + .padding(.vertical, 8) + .frame(maxWidth: .infinity) + .background(.bar) + .overlay(alignment: .bottom) { + Divider() + } + } + } + } + + @ViewBuilder + private func profileTabContent(tab: ProfileState.Tab) -> some View { + switch onEnum(of: tab) { + case .timeline(let timeline): + ProfileTimelineTabContent(presenter: timeline.presenter) + .id(profileTimelineID(for: tab)) + case .media(let media): + ProfileGalleryTabContent( + presenter: media.presenter, + accountType: accountType + ) + .id(profileTimelineID(for: tab)) + } + } + + private func clampedSelectedTabIndex(for tabs: [ProfileState.Tab]) -> Int { + min(max(selectedTab, 0), max(tabs.count - 1, 0)) + } + + private func normalizeSelectedTab(tabCount: Int) { + guard tabCount > 0 else { + selectedTab = 0 + return + } + selectedTab = min(max(selectedTab, 0), tabCount - 1) + } + + private func handleFollowAction(user: UiProfile, followButtonState: FollowButtonState) { + switch onEnum(of: followButtonState) { + case .blocked: + presenter.state.unblock(userKey: user.key) + case .following, .requested: + presenter.state.unfollow(userKey: user.key) + case .follow, .requestFollow: + presenter.state.follow(userKey: user.key) + } + } +} + +struct ProfileWithUserNameAndHostScreen: View { + @StateObject private var presenter: KotlinPresenter + let accountType: AccountType + let onFollowingClick: (MicroBlogKey) -> Void + let onFansClick: (MicroBlogKey) -> Void + let goBack: () -> Void + + init( + userName: String, + host: String, + accountType: AccountType, + onFollowingClick: @escaping (MicroBlogKey) -> Void, + onFansClick: @escaping (MicroBlogKey) -> Void, + goBack: @escaping () -> Void = {} + ) { + self.accountType = accountType + self.onFollowingClick = onFollowingClick + self.onFansClick = onFansClick + self.goBack = goBack + _presenter = .init( + wrappedValue: .init( + presenter: ProfileWithUserNameAndHostPresenter( + userName: userName, + host: host, + accountType: accountType + ) + ) + ) + } + + var body: some View { + StateView(state: presenter.state.user) { user in + ProfileScreen( + accountType: accountType, + userKey: user.key, + onFollowingClick: onFollowingClick, + onFansClick: onFansClick, + goBack: goBack + ) + } loadingContent: { + ProgressView() + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + .navigationTitle(LocalizedStrings.string("profile_title", fallback: "Profile")) + } +} + +private struct ProfileTimelineTabContent: View { + @StateObject private var presenter: KotlinPresenter + + init(presenter: TimelinePresenter) { + _presenter = .init(wrappedValue: .init(presenter: presenter)) + } + + var body: some View { + TimelinePagingView(data: presenter.state.listState) + } +} + +private struct ProfileGalleryTabContent: View { + private let accountType: AccountType + private let columns = [ + GridItem(.adaptive(minimum: 140, maximum: 240), spacing: 8) + ] + + @StateObject private var presenter: KotlinPresenter + + init( + presenter: ProfileMediaPresenter, + accountType: AccountType + ) { + self.accountType = accountType + _presenter = .init(wrappedValue: .init(presenter: presenter)) + } + + var body: some View { + content(data: presenter.state.mediaState) + } + + @ViewBuilder + private func content(data: PagingState) -> some View { + switch onEnum(of: data) { + case .empty: + ListEmptyView() + .padding() + case .error(let error): + ListErrorView(error: error.error) { + _ = error.onRetry() + } + .padding() + case .loading: + galleryGrid { + ForEach(0..<12, id: \.self) { _ in + ProfileGalleryPlaceholder() + } + } + case .success(let success): + successContent(success) + } + } + + @ViewBuilder + private func successContent(_ success: PagingStateSuccess) -> some View { + let count = Int(success.itemCount) + if count == 0 { + ListEmptyView() + .padding() + } else { + galleryGrid { + ForEach(0..(@ViewBuilder content: () -> Content) -> some View { + LazyVGrid(columns: columns, spacing: 8) { + content() + } + .padding(.horizontal) + } +} + +private struct ProfileGalleryTile: View { + @Environment(\.openURL) private var openURL + + let item: ProfileMedia + let accountType: AccountType + + var body: some View { + Color.clear + .aspectRatio(1, contentMode: .fit) + .overlay { + GeometryReader { proxy in + ZStack(alignment: .topTrailing) { + imageContent + .frame(width: proxy.size.width, height: proxy.size.height) + .clipped() + + mediaBadge + } + .frame(width: proxy.size.width, height: proxy.size.height) + } + } + .clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous)) + .overlay { + RoundedRectangle(cornerRadius: 8, style: .continuous) + .stroke(Color.flareSeparator.opacity(0.6), lineWidth: 1) + } + .contentShape(RoundedRectangle(cornerRadius: 8, style: .continuous)) + .onTapGesture { + openMedia() + } + } + + @ViewBuilder + private var imageContent: some View { + if let previewURL = item.media.profileGalleryPreviewURL { + NetworkImage( + data: previewURL, + customHeader: item.media.profileGalleryCustomHeaders + ) + } else { + ProfileGalleryPlaceholder() + } + } + + @ViewBuilder + private var mediaBadge: some View { + switch item.media.profileGalleryMediaKind { + case .image: + EmptyView() + case .video: + Image(systemName: "play.fill") + .profileGalleryBadgeStyle() + case .gif: + Text("GIF") + .font(.caption2.bold()) + .profileGalleryBadgeStyle() + case .audio: + Image(systemName: "waveform") + .profileGalleryBadgeStyle() + } + } + + private func openMedia() { + let route = DeeplinkRoute.MediaStatusMedia( + statusKey: item.statusKey, + accountType: accountType, + index: Int32(item.index), + preview: item.media.profileGalleryRoutePreviewURL + ) + if let url = URL(string: route.toUri()) { + openURL(url) + } + } +} + +private struct ProfileGalleryPlaceholder: View { + var body: some View { + Rectangle() + .fill(Color.flareSecondarySystemBackground) + .overlay { + Image(systemName: "photo") + .foregroundStyle(.secondary) + } + .aspectRatio(1, contentMode: .fit) + .redacted(reason: .placeholder) + } +} + +private enum ProfileGalleryMediaKind { + case image + case video + case gif + case audio +} + +private extension View { + func profileGalleryBadgeStyle() -> some View { + self + .foregroundStyle(.white) + .padding(6) + .background(.black.opacity(0.62), in: Capsule()) + .padding(6) + } +} + +private extension UiMedia { + var profileGalleryMediaKind: ProfileGalleryMediaKind { + switch onEnum(of: self) { + case .image: + .image + case .video: + .video + case .gif: + .gif + case .audio: + .audio + } + } + + var profileGalleryPreviewURL: String? { + switch onEnum(of: self) { + case .image(let image): + image.previewUrl.isEmpty ? image.url : image.previewUrl + case .video(let video): + video.thumbnailUrl.isEmpty ? video.url : video.thumbnailUrl + case .gif(let gif): + gif.previewUrl.isEmpty ? gif.url : gif.previewUrl + case .audio(let audio): + audio.previewUrl + } + } + + var profileGalleryRoutePreviewURL: String? { + switch onEnum(of: self) { + case .image(let image): + image.previewUrl + case .video(let video): + video.thumbnailUrl + case .gif(let gif): + gif.previewUrl + case .audio: + nil + } + } + + var profileGalleryCustomHeaders: [String: String]? { + switch onEnum(of: self) { + case .image(let image): + image.customHeaders + case .video(let video): + video.customHeaders + case .gif(let gif): + gif.customHeaders + case .audio(let audio): + audio.customHeaders + } + } +} diff --git a/appleApp/macos/UI/Screen/ServiceSelectionScreen.swift b/appleApp/macos/UI/Screen/ServiceSelectionScreen.swift new file mode 100644 index 0000000000..effc9083af --- /dev/null +++ b/appleApp/macos/UI/Screen/ServiceSelectionScreen.swift @@ -0,0 +1,740 @@ +import SwiftUI +import AuthenticationServices +import Combine +import FlareAppleCore +import FlareAppleUI +import AppleFontAwesome +@preconcurrency import KotlinSharedUI +import WebKit + +private enum MacOSServiceSelectionAnimation { + static let standard: Animation = .easeInOut(duration: 0.2) + static let inline: AnyTransition = .opacity.combined(with: .scale(scale: 0.98)) +} + +struct ServiceSelectionScreen: View { + @Environment(\.webAuthenticationSession) private var webAuthenticationSession + + let toHome: () -> Void + + @StateObject private var presenter: KotlinPresenter + @State private var instanceInput = "" + @State private var selectedMethods: [String: LoginMethodType] = [:] + + init(toHome: @escaping () -> Void) { + self.toHome = toHome + self._presenter = .init(wrappedValue: .init(presenter: ServiceSelectPresenter(toHome: toHome))) + } + + var body: some View { + Form { + Section { + header + } + + Section { + instanceInputView(state: presenter.state) + } footer: { + Text(ServiceSelectCopy.welcomeHint) + } + + if let node = detectedNode(from: presenter.state.detectedPlatformType), presenter.state.canNext { + Section { + loginContent(state: presenter.state, node: node) + .id("login-\(node.platformType)-\(node.host)") + .transition(MacOSServiceSelectionAnimation.inline) + } + } else { + Section { + recommendedInstances(state: presenter.state) + .id("recommendations") + .transition(MacOSServiceSelectionAnimation.inline) + } header: { + Text(ServiceSelectCopy.welcomeListHint) + } + } + } + .formStyle(.grouped) + .animation(MacOSServiceSelectionAnimation.standard, value: serviceContentKey(state: presenter.state)) + .onChange(of: instanceInput) { _, newValue in + presenter.state.setFilter(value: newValue) + } + } + + private var header: some View { + VStack(alignment: .leading, spacing: 8) { + Text(ServiceSelectCopy.welcomeTitle) + .font(.title2.weight(.semibold)) + Text(ServiceSelectCopy.welcomeMessage) + .font(.body) + .foregroundStyle(.secondary) + } + .frame(maxWidth: .infinity, alignment: .leading) + } + + private func instanceInputView(state: ServiceSelectState) -> some View { + HStack(spacing: 10) { + platformIndicator(state: state) + .frame(width: 20, height: 20) + + TextField(ServiceSelectCopy.instancePlaceholder, text: $instanceInput) + .textFieldStyle(.roundedBorder) + .disabled(state.loading) + .onSubmit { + state.setFilter(value: instanceInput) + } + + Button { + instanceInput = "" + state.setFilter(value: "") + selectedMethods.removeAll() + } label: { + Image(fontAwesome: instanceInput.isEmpty ? .magnifyingGlass : .xmark) + .resizable() + .scaledToFit() + .frame(width: 14, height: 14) + } + .buttonStyle(.borderless) + .foregroundStyle(.secondary) + .disabled(instanceInput.isEmpty) + .help(instanceInput.isEmpty ? ServiceSelectCopy.search : ServiceSelectCopy.clear) + } + } + + @ViewBuilder + private func platformIndicator(state: ServiceSelectState) -> some View { + ZStack { + switch onEnum(of: state.detectedPlatformType) { + case .success(let success): + Image(fontAwesome: state.platformIcon(platformType: success.data.platformType).fontAwesomeIcon) + .resizable() + .scaledToFit() + case .loading: + ProgressView() + .controlSize(.small) + case .error: + Image(systemName: "questionmark.circle") + .foregroundStyle(.secondary) + } + } + .animation(MacOSServiceSelectionAnimation.standard, value: platformIndicatorKey(state: state)) + } + + @ViewBuilder + private func loginContent(state: ServiceSelectState, node: NodeData) -> some View { + let methods = state.loginMethods(platformType: node.platformType) + if let firstMethod = methods.first { + let key = "\(node.platformType)-\(node.host)" + let selectedMethod = Binding( + get: { selectedMethods[key] ?? firstMethod.type }, + set: { method in + withAnimation(MacOSServiceSelectionAnimation.standard) { + selectedMethods[key] = method + } + } + ) + + VStack(alignment: .leading, spacing: 12) { + platformHeader(state: state, node: node) { + if methods.count > 1 { + Picker("", selection: selectedMethod) { + ForEach(methods.indices, id: \.self) { index in + Text(methods[index].title.text).tag(methods[index].type) + } + } + .pickerStyle(.menu) + .labelsHidden() + } + } + + if node.compatibleMode { + Text(ServiceSelectCopy.compatibilityWarning(node.software)) + .font(.caption) + .foregroundStyle(.secondary) + } + + let handler = state.createLoginHandler( + platformType: node.platformType, + host: node.host, + methodType: selectedMethod.wrappedValue, + redirectUri: nil, + ) + + LoginFlowView(handler: handler, authenticateURL: authenticate(url:)) + .id("\(key)-\(selectedMethod.wrappedValue)") + + LoginAgreementView( + urlString: state.agreementUrl(platformType: node.platformType, host: node.host) + ) + } + .frame(maxWidth: .infinity, alignment: .leading) + .animation(MacOSServiceSelectionAnimation.standard, value: "\(selectedMethod.wrappedValue)") + } + } + + private func platformHeader( + state: ServiceSelectState, + node: NodeData, + @ViewBuilder trailing: () -> Trailing + ) -> some View { + HStack(spacing: 8) { + Image(fontAwesome: state.platformIcon(platformType: node.platformType).fontAwesomeIcon) + .resizable() + .scaledToFit() + .frame(width: 22, height: 22) + + VStack(alignment: .leading, spacing: 2) { + Text(platformTitle(node.platformType)) + .font(.headline) + Text(node.host) + .font(.caption) + .foregroundStyle(.secondary) + } + + Spacer() + + trailing() + } + } + + private func recommendedInstances(state: ServiceSelectState) -> some View { + PagingView( + data: state.instances, + maxCount: 20, + successContent: { instance in + ServiceInstanceRow( + instance: instance, + icon: state.platformIcon(platformType: instance.type).fontAwesomeIcon + ) { + select(instance: instance, state: state) + } + }, + loadingContent: { + ServiceInstancePlaceholderRow() + }, + errorContent: { error, retry in + ListErrorView(error: error, onRetry: retry) + }, + emptyContent: { + Text(ServiceSelectCopy.emptyMessage) + .font(.headline) + .foregroundStyle(.secondary) + .frame(maxWidth: .infinity) + .padding(.vertical, 16) + } + ) + } + + private func select(instance: UiInstance, state: ServiceSelectState) { + withAnimation(MacOSServiceSelectionAnimation.standard) { + instanceInput = instance.domain + state.setFilter(value: instance.domain) + selectedMethods.removeAll() + } + } + + private func detectedNode(from state: UiState) -> NodeData? { + guard case .success(let success) = onEnum(of: state) else { + return nil + } + return success.data + } + + private func serviceContentKey(state: ServiceSelectState) -> String { + if let node = detectedNode(from: state.detectedPlatformType), state.canNext { + return "login-\(node.platformType)-\(node.host)-\(node.compatibleMode)" + } + return "recommendations" + } + + private func platformIndicatorKey(state: ServiceSelectState) -> String { + switch onEnum(of: state.detectedPlatformType) { + case .success(let success): + return "success-\(success.data.platformType)-\(success.data.host)" + case .loading: + return "loading" + case .error: + return "error" + } + } + + private func platformTitle(_ type: PlatformType) -> String { + switch type { + case .mastodon: + return "Mastodon" + case .misskey: + return "Misskey" + case .bluesky: + return "Bluesky" + case .nostr: + return "Nostr" + case .xQt: + return "X" + case .vvo: + return "Weibo" + case .pixiv: + return "Pixiv" + } + } + + private func authenticate(url: String) async -> String? { + guard let authURL = URL(string: url) else { + return nil + } + let response = try? await webAuthenticationSession.authenticate( + using: authURL, + callbackURLScheme: authURL.isPixivOAuthUrl ? "pixiv" : APPSCHEMA + ) + return response?.absoluteString + } +} + +private struct LoginFlowView: View { + let authenticateURL: (String) async -> String? + + @StateObject private var presenter: KotlinPresenter + @State private var qrContent: String? + @State private var webCookieUrl: String? + + init( + handler: LoginMethodHandler, + authenticateURL: @escaping (String) async -> String? + ) { + self.authenticateURL = authenticateURL + self._presenter = .init(wrappedValue: .init(presenter: LoginFlowPresenter(handler: handler))) + } + + var body: some View { + VStack(alignment: .leading, spacing: 10) { + ForEach(presenter.state.flowState.fields, id: \.id) { field in + LoginFieldView(field: field) { id, value in + presenter.state.updateField(id: id, value: value) + } onSubmit: { + if let action = presenter.state.flowState.actions.first(where: { $0.enabled }) { + presenter.state.perform(actionId: action.id) + } + } + } + + if let qrContent { + QRLoginView(content: qrContent) + .frame(maxWidth: .infinity) + .transition(MacOSServiceSelectionAnimation.inline) + } + + ForEach(presenter.state.flowState.actions, id: \.id) { action in + Button { + presenter.state.perform(actionId: action.id) + if action.label == .cancel { + withAnimation(MacOSServiceSelectionAnimation.standard) { + qrContent = nil + } + } + } label: { + progressButtonLabel(title: action.label.text, isLoading: presenter.state.flowState.loading) + } + .buttonStyle(.borderedProminent) + .controlSize(.large) + .frame(maxWidth: .infinity) + .disabled(!action.enabled || presenter.state.flowState.loading) + } + + LoginErrorText(message: presenter.state.flowState.error) + } + .animation(MacOSServiceSelectionAnimation.standard, value: flowAnimationKey) + .onReceive( + presenter.state.effects + .toPublisher() + .catch { _ in Empty() } + .receive(on: DispatchQueue.main) + ) { effect in + switch onEnum(of: effect) { + case .openUrl(let openUrl): + authenticate(url: openUrl.url) + case .showQr(let showQr): + withAnimation(MacOSServiceSelectionAnimation.standard) { + qrContent = showQr.content + } + case .openWebCookieLogin(let webCookie): + webCookieUrl = webCookie.url + } + } + .sheet(isPresented: Binding( + get: { webCookieUrl != nil }, + set: { value in + if !value { + webCookieUrl = nil + } + } + )) { + if let webCookieUrl { + MacOSWebLoginScreen(onCookie: { cookie in + guard presenter.state.canResume(value: cookie) else { + return + } + presenter.state.resume(value: cookie) + self.webCookieUrl = nil + }, url: webCookieUrl) + } + } + } + + private func progressButtonLabel(title: String, isLoading: Bool) -> some View { + HStack(spacing: 8) { + if isLoading { + ProgressView() + .controlSize(.small) + } + Text(title) + } + .frame(maxWidth: .infinity) + } + + private var flowAnimationKey: String { + let flowState = presenter.state.flowState + let fields = + flowState.fields + .map { "\($0.id):\($0.type):\($0.readOnly)" } + .joined(separator: ",") + let actions = + flowState.actions + .map { "\($0.id):\($0.label):\($0.enabled)" } + .joined(separator: ",") + return [ + fields, + actions, + "\(flowState.loading)", + flowState.error ?? "", + qrContent ?? "", + ].joined(separator: "|") + } + + private func authenticate(url: String) { + Task { + if let responseString = await authenticateURL(url) { + presenter.state.resume(value: responseString) + } + } + } +} + +private struct LoginFieldView: View { + let field: LoginField + let onUpdate: (String, String) -> Void + let onSubmit: () -> Void + + @State private var value: String + + init( + field: LoginField, + onUpdate: @escaping (String, String) -> Void, + onSubmit: @escaping () -> Void + ) { + self.field = field + self.onUpdate = onUpdate + self.onSubmit = onSubmit + self._value = .init(initialValue: field.value) + } + + var body: some View { + VStack(alignment: .leading, spacing: 6) { + switch field.type { + case .passwordInput: + SecureField(field.label.text, text: $value) + .textFieldStyle(.roundedBorder) + case .displayText: + Text(field.value) + .font(.caption) + .foregroundStyle(.secondary) + default: + TextField(field.label.text, text: $value) + .textFieldStyle(.roundedBorder) + .textContentType(field.id == "username" ? .username : nil) + } + + LoginErrorText(message: field.error) + } + .disabled(field.readOnly) + .onChange(of: value) { _, newValue in + onUpdate(field.id, newValue) + } + .onSubmit { + onSubmit() + } + } +} + +private struct QRLoginView: View { + let content: String + + var body: some View { + VStack(spacing: 10) { + Text(ServiceSelectCopy.nostrQRHint) + .font(.caption) + .foregroundStyle(.secondary) + .multilineTextAlignment(.center) + QRCodeView(text: content) + .frame(width: 200, height: 200) + .background(.white, in: RoundedRectangle(cornerRadius: 8)) + HStack(spacing: 8) { + ProgressView() + .controlSize(.small) + Text(ServiceSelectCopy.nostrQRWaiting) + .font(.caption) + } + Text(ServiceSelectCopy.nostrQRLinkLabel) + .font(.caption) + .foregroundStyle(.secondary) + Text(content) + .font(.caption2) + .textSelection(.enabled) + .multilineTextAlignment(.center) + .lineLimit(4) + } + } +} + +private struct ServiceInstanceRow: View { + let instance: UiInstance + let icon: FontAwesomeIcon + let onSelect: () -> Void + + var body: some View { + Button(action: onSelect) { + HStack(spacing: 10) { + instanceIcon + .frame(width: 28, height: 28) + + VStack(alignment: .leading, spacing: 2) { + Text(instance.name) + .lineLimit(1) + Text(instance.domain) + .font(.caption) + .foregroundStyle(.secondary) + .lineLimit(1) + if let description = instance.description_, !description.isEmpty { + Text(description) + .font(.caption) + .foregroundStyle(.secondary) + .lineLimit(2) + } + } + + Spacer() + } + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + } + + @ViewBuilder + private var instanceIcon: some View { + if let iconUrl = instance.iconUrl, !iconUrl.isEmpty { + NetworkImage(data: iconUrl) + .clipShape(RoundedRectangle(cornerRadius: 4)) + } else { + Image(fontAwesome: icon) + .resizable() + .scaledToFit() + } + } +} + +private struct ServiceInstancePlaceholderRow: View { + var body: some View { + HStack(spacing: 10) { + Image(systemName: "questionmark.circle") + .frame(width: 28, height: 28) + VStack(alignment: .leading, spacing: 4) { + Text("Placeholder") + Text("placeholder") + .font(.caption) + Text("Description Placeholder") + .font(.caption) + } + Spacer() + } + .redacted(reason: .placeholder) + } +} + +private struct LoginErrorText: View { + let message: String? + + var body: some View { + if let message, !message.isEmpty { + Text(message) + .font(.caption) + .foregroundStyle(.red) + .frame(maxWidth: .infinity, alignment: .leading) + } + } +} + +private struct LoginAgreementView: View { + let urlString: String? + + var body: some View { + if let urlString, let url = URL(string: urlString) { + Text(agreement(url: url)) + .font(.caption) + .foregroundStyle(.secondary) + } + } + + private func agreement(url: URL) -> AttributedString { + var text = AttributedString(ServiceSelectCopy.loginAgreementPrefix + ServiceSelectCopy.eulaPrivacyPolicy + ".") + if let range = text.range(of: ServiceSelectCopy.eulaPrivacyPolicy) { + text[range].link = url + text[range].foregroundColor = .accentColor + } + return text + } +} + +private struct MacOSWebLoginScreen: View { + @Environment(\.dismiss) private var dismiss + @StateObject private var viewModel: MacOSWebLoginViewModel + private let url: String + + init(onCookie: @escaping (String) -> Void, url: String) { + self._viewModel = .init(wrappedValue: .init(onCookie: onCookie, url: url)) + self.url = url + } + + var body: some View { + VStack(spacing: 0) { + HStack { + Spacer() + Button { + dismiss() + } label: { + Image(systemName: "xmark") + } + .buttonStyle(.plain) + .help(LocalizedStrings.string("compose_button_cancel", fallback: "Cancel")) + } + .padding(12) + + if viewModel.canShowWebView { + MacOSWebView(url: URL(string: url), configuration: viewModel.configuration) { webView in + webView.navigationDelegate = viewModel.delegate + } + } else { + ProgressView() + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + } + } +} + +private struct MacOSWebView: NSViewRepresentable { + let url: URL? + let configuration: WKWebViewConfiguration + let configure: (WKWebView) -> Void + + func makeNSView(context: Context) -> WKWebView { + let webView = WKWebView(frame: .zero, configuration: configuration) + configure(webView) + if let url { + webView.load(URLRequest(url: url)) + } + return webView + } + + func updateNSView(_ nsView: WKWebView, context: Context) {} +} + +private final class MacOSCookieNavigationDelegate: NSObject, WKNavigationDelegate { + private let onNavigationResponse: () -> Void + + init(onNavigationResponse: @escaping () -> Void) { + self.onNavigationResponse = onNavigationResponse + } + + func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse) async -> WKNavigationResponsePolicy { + onNavigationResponse() + return .allow + } +} + +private final class MacOSWebLoginViewModel: ObservableObject { + @Published var canShowWebView = false + + let delegate: MacOSCookieNavigationDelegate + + init(onCookie: @escaping (String) -> Void, url: String) { + self.delegate = MacOSCookieNavigationDelegate { + WKWebsiteDataStore.default().httpCookieStore.getAllCookies { cookies in + let cookieString = MacOSWebLoginViewModel.cookieHeaderString(from: cookies, for: URL(string: url)) + onCookie(cookieString) + } + } + clearCookie() + } + + var configuration: WKWebViewConfiguration { + let configuration = WKWebViewConfiguration() + configuration.defaultWebpagePreferences.allowsContentJavaScript = true + return configuration + } + + private func clearCookie() { + let dataStore = WKWebsiteDataStore.default() + dataStore.fetchDataRecords(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes()) { records in + dataStore.removeData( + ofTypes: WKWebsiteDataStore.allWebsiteDataTypes(), + for: records, + completionHandler: { + self.canShowWebView = true + } + ) + } + } + + private static func cookieHeaderString(from cookies: [HTTPCookie], for url: URL?) -> String { + let host = url?.host?.lowercased() + let filtered = cookies.filter { cookie in + guard let host else { + return true + } + let domain = cookie.domain.lowercased() + return domain == host || (domain.hasPrefix(".") && (domain.hasSuffix(host) || host.hasSuffix(domain))) + } + return filtered.map { "\($0.name)=\($0.value)" }.joined(separator: "; ") + } +} + +private extension URL { + var isPixivOAuthUrl: Bool { + scheme == "https" && + host == "app-api.pixiv.net" && + path == "/web/v1/login" + } +} + +private enum ServiceSelectCopy { + static let welcomeTitle = String(localized: "service_select_welcome_title", defaultValue: "Welcome to Flare") + static let welcomeMessage = String(localized: "service_select_welcome_message", defaultValue: "Enter a server to get started.") + static let welcomeHint = String(localized: "service_select_welcome_hint", defaultValue: "Flare supports Mastodon, Misskey, Bluesky, Nostr, and X.") + static let welcomeListHint = String(localized: "service_select_welcome_list_hint", defaultValue: "Or choose from these servers") + static let instancePlaceholder = String(localized: "service_select_instance_input_placeholder", defaultValue: "Instance URL") + static let emptyMessage = String(localized: "service_select_empty_message", defaultValue: "No instances found") + static let nostrQRHint = String(localized: "nostr_login_qr_hint", defaultValue: "Scan with Amber, Alby, or another Nostr Connect signer.") + static let nostrQRWaiting = String(localized: "nostr_login_qr_waiting", defaultValue: "Waiting for signer approval...") + static let nostrQRLinkLabel = String(localized: "nostr_login_qr_link_label", defaultValue: "Nostr Connect link") + static let eulaPrivacyPolicy = String(localized: "eula_privacy_policy", defaultValue: "EULA and Privacy Policy") + static let loginAgreementPrefix = String(localized: "login_agreement_prefix", defaultValue: "By logging in, you agree to the ") + static let search = String(localized: "Search") + static let clear = String(localized: "Clear") + + static func compatibilityWarning(_ software: String) -> String { + String( + format: String( + localized: "service_select_compatibility_warning", + defaultValue: "This server uses %@, Flare will run in compatibility mode. Some features may not work properly." + ), + software + ) + } +} diff --git a/appleApp/macos/UI/Screen/StatusDetailScreen.swift b/appleApp/macos/UI/Screen/StatusDetailScreen.swift new file mode 100644 index 0000000000..2a79e0661f --- /dev/null +++ b/appleApp/macos/UI/Screen/StatusDetailScreen.swift @@ -0,0 +1,45 @@ +import AppKit +import FlareAppleCore +import FlareAppleUI +@preconcurrency import KotlinSharedUI +import SwiftUI + +struct StatusDetailScreen: View { + @Environment(\.timelineAppearance.timelineDisplayMode) private var timelineDisplayMode + + @StateObject private var presenter: KotlinPresenter + private let statusKey: MicroBlogKey + + init(accountType: AccountType, statusKey: MicroBlogKey) { + self.statusKey = statusKey + _presenter = .init( + wrappedValue: .init( + presenter: StatusContextPresenter(accountType: accountType, statusKey: statusKey) + ) + ) + } + + var body: some View { + ScrollView { + LazyVStack(spacing: 0) { + TimelinePagingView( + data: presenter.state.listState, + detailStatusKey: statusKey + ) + } + .frame(maxWidth: 640, alignment: .center) + .frame(maxWidth: .infinity, alignment: .center) + .padding(.bottom, 12) + } + .id(presenter.key) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(detailBackground) + .navigationTitle("status_detail_title") + } + + private var detailBackground: Color { + timelineDisplayMode == .plain + ? .clear + : Color(nsColor: .windowBackgroundColor) + } +} diff --git a/appleApp/macos/UI/Screen/TimelineScreen.swift b/appleApp/macos/UI/Screen/TimelineScreen.swift new file mode 100644 index 0000000000..fb2745a440 --- /dev/null +++ b/appleApp/macos/UI/Screen/TimelineScreen.swift @@ -0,0 +1,35 @@ +import FlareAppleCore +@preconcurrency import KotlinSharedUI +import SwiftUI + +struct TimelineScreen: View { + let tabItem: UiTimelineTabItem + let allowGalleryMode: Bool + @StateObject private var presenter: KotlinPresenter + + init(tabItem: UiTimelineTabItem, allowGalleryMode: Bool = false) { + self.tabItem = tabItem + self.allowGalleryMode = allowGalleryMode + _presenter = .init(wrappedValue: .init(presenter: TimelineItemPresenter(timelineTabItem: tabItem))) + } + + var body: some View { + TimelinePagingContent( + data: presenter.state.listState, + detailStatusKey: nil, + key: presenter.key, + allowGalleryMode: allowGalleryMode + ) + .refreshable { + try? await presenter.state.refreshSuspend() + } + } +} + +struct ListTimelineScreen: View { + let tabItem: UiTimelineTabItem + + var body: some View { + TimelineScreen(tabItem: tabItem) + } +} diff --git a/appleApp/macos/en.lproj/Localizable.strings b/appleApp/macos/en.lproj/Localizable.strings new file mode 100644 index 0000000000..9ca8fb8ff8 --- /dev/null +++ b/appleApp/macos/en.lproj/Localizable.strings @@ -0,0 +1,208 @@ +"macos_sidebar_timelines" = "Timelines"; +"macos_sidebar_accounts" = "Accounts"; +"macos_sidebar_library" = "Library"; +"macos_account_title" = "Account"; +"macos_account_unavailable" = "Unavailable"; +"macos_account_add" = "Add an account"; +"macos_loading" = "Loading"; +"macos_placeholder_account" = "Account switching and login entry point."; +"macos_placeholder_home" = "Timeline content will render here."; +"macos_placeholder_discover" = "Search and discovery content will render here."; +"macos_placeholder_notifications" = "Notification content will render here."; +"macos_placeholder_compose" = "Composer content will render here."; +"macos_placeholder_drafts" = "Draft content will render here."; +"macos_placeholder_rss" = "RSS source management will render here."; +"macos_placeholder_history" = "Local history content will render here."; +"macos_placeholder_agent_history" = "Agent conversation history will render here."; +"macos_placeholder_settings" = "Application settings will render here."; +"macos_placeholder_secondary_tab" = "Account shortcut content will render here."; +"search" = "Search"; +"home_compose" = "Compose"; +"draft_box_title" = "Drafts"; +"home_tab_home_title" = "Home"; +"home_tab_notifications_title" = "Notifications"; +"home_tab_discover_title" = "Discover"; +"home_tab_me_title" = "Me"; +"settings_title" = "Settings"; +"settings_rss_management_title" = "Subscription Management"; +"settings_agent_history_title" = "Flare AI"; +"local_history_title" = "Local History"; +"mastodon_tab_local_title" = "Local"; +"mastodon_tab_public_title" = "Public"; +"home_tab_featured_title" = "Featured"; +"home_tab_bookmarks_title" = "Bookmarks"; +"home_tab_favorite_title" = "Favorites"; +"home_tab_list_title" = "Lists"; +"home_tab_feeds_title" = "Feeds"; +"dm_list_title" = "Direct Messages"; +"rss_title" = "Subscriptions"; +"social_title" = "Social"; +"antenna_title" = "Antennas"; +"mixed_timeline_title" = "Mixed"; +"liked_tab_title" = "Liked"; +"all_rss_feeds_title" = "All Subscriptions"; +"posts_title" = "Posts"; +"posts_with_replies_title" = "Posts & Replies"; +"media_title" = "Media"; +"channel_title" = "Channel"; +"tab_settings_default" = "Default"; +"login_button" = "Log in"; +"verify_button" = "Verify"; +"cancel_button" = "Cancel"; +"ok_button" = "Ok"; +"delete_button" = "Delete"; +"service_select_next_button" = "Next"; +"bluesky_login_username_hint" = "Username"; +"bluesky_login_password_hint" = "Password"; +"bluesky_login_auth_factor_token_hint" = "2FA token"; +"bluesky_login_oauth_button" = "Log in with OAuth"; +"bluesky_login_use_password_button" = "Use password"; +"nostr_login_title" = "Import Nostr account"; +"nostr_login_account_hint" = "npub, nsec, hex key, bunker://, or nostrconnect://"; +"nostr_login_amber_button" = "Connect Amber"; +"nostr_login_qr_button" = "Connect with QR"; +"misskey_channel_tab_following" = "Following"; +"pixiv_ranking_week_title" = "Weekly Ranking"; +"pixiv_ranking_month_title" = "Monthly Ranking"; +"pixiv_ranking_day_male_title" = "Male Ranking"; +"pixiv_ranking_day_female_title" = "Female Ranking"; +"pixiv_ranking_week_original_title" = "Original Ranking"; +"pixiv_ranking_week_rookie_title" = "Rookie Ranking"; +"pixiv_ranking_day_manga_title" = "Manga Ranking"; +"illustrations_title" = "Illustrations"; +"manga_title" = "Manga"; +"macos_settings_section_account" = "Account"; +"macos_settings_section_appearance" = "Appearance"; +"macos_settings_section_content" = "Content"; +"macos_settings_section_intelligence" = "AI & Translation"; +"macos_settings_section_app" = "App"; +"macos_action_clear" = "Clear"; +"macos_action_open" = "Open"; +"macos_action_export" = "Export"; +"macos_action_import" = "Import"; +"macos_action_edit" = "Edit"; +"macos_action_run" = "Run"; +"macos_action_save" = "Save"; +"macos_action_move_up" = "Move Up"; +"macos_action_move_down" = "Move Down"; +"account_management_title" = "Accounts"; +"account_management_description" = "Manage signed-in accounts, active account, and account order."; +"appearance_description" = "Theme, layout, display, and media settings."; +"appearance_theme_group_title" = "Theme"; +"appearance_theme_group_subtitle" = "Adjust app theme and avatar shape."; +"appearance_layout_group_title" = "Layout"; +"appearance_layout_group_subtitle" = "Control timeline layout and post action placement."; +"appearance_display_group_title" = "Display"; +"appearance_display_group_subtitle" = "Tune timestamps, platform marks, previews, and browser behavior."; +"appearance_media_group_title" = "Media"; +"appearance_media_group_subtitle" = "Control media visibility, sensitive content, warnings, and autoplay."; +"appearance_theme" = "Theme"; +"appearance_theme_description" = "Choose how Flare follows system appearance."; +"appearance_theme_system" = "System"; +"appearance_theme_light" = "Light"; +"appearance_theme_dark" = "Dark"; +"appearance_avatar_shape" = "Avatar shape"; +"appearance_avatar_shape_description" = "Choose the shape used for account avatars."; +"appearance_avatar_shape_circle" = "Circle"; +"appearance_avatar_shape_square" = "Square"; +"appearance_timeline_display_mode" = "Timeline display mode"; +"appearance_timeline_display_mode_description" = "Choose the default timeline presentation."; +"appearance_timeline_display_mode_card" = "Card"; +"appearance_timeline_display_mode_plain" = "Plain"; +"appearance_timeline_display_mode_gallery" = "Gallery"; +"appearance_show_bottom_bar_labels" = "Show tab labels"; +"appearance_show_bottom_bar_labels_description" = "Show text labels for main navigation tabs."; +"appearance_deck_mode" = "Deck mode"; +"appearance_deck_mode_description" = "Use multi-column deck presentation where available."; +"appearance_fullWidthPost" = "Full-width posts"; +"appearance_fullWidthPost_description" = "Let post content use the full row width."; +"appearance_post_action_style" = "Post action style"; +"appearance_post_action_style_description" = "Choose how reply, boost, like, and share actions are arranged."; +"appearance_post_action_style_hidden" = "Hidden"; +"appearance_post_action_style_left_aligned" = "Left aligned"; +"appearance_post_action_style_right_aligned" = "Right aligned"; +"appearance_post_action_style_stretch" = "Stretch"; +"appearance_show_numbers" = "Show counts"; +"appearance_show_numbers_description" = "Show counts beside post actions."; +"appearance_absolute_timestamp" = "Absolute timestamps"; +"appearance_absolute_timestamp_description" = "Show exact timestamps instead of relative time."; +"appearance_show_platform_logo" = "Show platform logo"; +"appearance_show_platform_logo_description" = "Display the source platform mark on posts."; +"appearance_show_link_preview" = "Show link previews"; +"appearance_show_link_preview_description" = "Display preview cards for links."; +"appearance_compat_link_preview" = "Compatibility link previews"; +"appearance_compat_link_preview_description" = "Use a more compatible preview layout."; +"appearance_in_app_browser" = "Open links in app"; +"appearance_in_app_browser_description" = "Open supported links inside Flare."; +"appearance_show_media" = "Show media"; +"appearance_show_media_description" = "Display images and videos in timelines."; +"appearance_expand_media_size" = "Expand media size"; +"appearance_expand_media_size_description" = "Use larger media previews where possible."; +"appearance_show_sensitive_content" = "Show sensitive content"; +"appearance_show_sensitive_content_description" = "Reveal media marked as sensitive by default."; +"appearance_expand_content_warning" = "Expand content warnings"; +"appearance_expand_content_warning_description" = "Show content behind warnings by default."; +"appearance_video_autoplay" = "Video autoplay"; +"appearance_video_autoplay_description" = "Choose when videos play automatically."; +"appearance_video_autoplay_never" = "Never"; +"appearance_video_autoplay_wifi" = "Wi-Fi"; +"appearance_video_autoplay_always" = "Always"; +"system_settings_title" = "System Settings"; +"system_settings_description" = "Open macOS System Settings for app permissions and system-level options."; +"local_filter_title" = "Local filters"; +"local_filter_description" = "Hide matching content locally."; +"local_filter_edit_title" = "Add filter"; +"local_filter_edit" = "Edit"; +"local_filter_delete" = "Delete"; +"local_filter_keyword_header" = "Keyword"; +"local_filter_keyword_placeholder" = "Keyword or regular expression"; +"local_filter_scope_header" = "Scope"; +"local_filter_timeline" = "Timeline"; +"local_filter_notification" = "Notifications"; +"local_filter_search" = "Search"; +"local_filter_regex" = "Regular expression"; +"storage_title" = "Storage"; +"storage_description" = "Clear caches, inspect logs, and import or export data."; +"storage_clear_image_cache" = "Clear image cache"; +"storage_clear_image_cache_desc" = "Remove cached images from URLCache, Kingfisher, and WebKit."; +"storage_clear_image_cache_confirm" = "Clear image cache?"; +"storage_clear_database_cache" = "Clear database cache"; +"storage_clear_database_cache_confirm" = "Clear database cache?"; +"storage_view_app_log" = "View app log"; +"storage_view_app_log_desc" = "Inspect captured network and application log messages."; +"settings_storage_export_data" = "Export data"; +"settings_storage_export_data_desc" = "Export app data and settings as JSON."; +"settings_storage_import_data" = "Import data"; +"settings_storage_import_data_desc" = "Import a previously exported JSON file."; +"import_confirmation_title" = "Import data?"; +"import_confirmation_message" = "Importing replaces local data and settings with the selected file."; +"import_completed" = "Import completed"; +"import_error" = "Import failed"; +"export_error" = "Export failed"; +"save_completed" = "Saved"; +"save_error" = "Save failed"; +"app_log" = "App Log"; +"app_log_network_toggle" = "Capture network log"; +"ai_config_title" = "AI"; +"ai_config_description" = "Configure AI provider, model, summaries, and insights."; +"ai_config_post_insight" = "Post insight"; +"ai_config_post_insight_description" = "Enable AI-powered post analysis."; +"ai_config_summarize" = "Summaries"; +"ai_config_pre_translate" = "Pre-translate"; +"ai_config_pre_translate_description" = "Translate eligible content automatically before display."; +"settings_translation_title" = "Translation"; +"settings_translation_description" = "Configure translation provider and automatic translation behavior."; +"settings_translation_test_description" = "Run the sample with the current provider settings."; +"about_title" = "About"; +"about_description" = "Version, project links, and community resources."; +"settings_about_description" = "A multi-service social client."; +"settings_about_source_code" = "Source Code"; +"settings_about_telegram" = "Telegram"; +"settings_about_telegram_description" = "Join the Telegram community."; +"settings_about_discord" = "Discord"; +"settings_about_discord_description" = "Join the Discord community."; +"settings_about_localization" = "Localization"; +"settings_about_localization_description" = "Help translate Flare."; +"settings_privacy_policy" = "Privacy Policy"; +"logout_title" = "Log out"; +"list_empty_title" = "No items"; diff --git a/appleApp/macos/zh-Hans.lproj/Localizable.strings b/appleApp/macos/zh-Hans.lproj/Localizable.strings new file mode 100644 index 0000000000..eaa8f2df9d --- /dev/null +++ b/appleApp/macos/zh-Hans.lproj/Localizable.strings @@ -0,0 +1,208 @@ +"macos_sidebar_timelines" = "时间线"; +"macos_sidebar_accounts" = "账号"; +"macos_sidebar_library" = "资料库"; +"macos_account_title" = "账号"; +"macos_account_unavailable" = "不可用"; +"macos_account_add" = "添加账号"; +"macos_loading" = "加载中"; +"macos_placeholder_account" = "账号切换和登录入口会显示在这里。"; +"macos_placeholder_home" = "时间线内容会显示在这里。"; +"macos_placeholder_discover" = "搜索和发现内容会显示在这里。"; +"macos_placeholder_notifications" = "通知内容会显示在这里。"; +"macos_placeholder_compose" = "发布编辑器会显示在这里。"; +"macos_placeholder_drafts" = "草稿内容会显示在这里。"; +"macos_placeholder_rss" = "RSS 订阅源管理会显示在这里。"; +"macos_placeholder_history" = "本地历史内容会显示在这里。"; +"macos_placeholder_agent_history" = "Agent 对话历史会显示在这里。"; +"macos_placeholder_settings" = "应用设置会显示在这里。"; +"macos_placeholder_secondary_tab" = "账号快捷入口内容会显示在这里。"; +"search" = "搜索"; +"home_compose" = "发布"; +"draft_box_title" = "草稿箱"; +"home_tab_home_title" = "主页"; +"home_tab_notifications_title" = "通知"; +"home_tab_discover_title" = "发现"; +"home_tab_me_title" = "我"; +"settings_title" = "设置"; +"settings_rss_management_title" = "订阅管理"; +"settings_agent_history_title" = "Flare AI"; +"local_history_title" = "本地历史"; +"mastodon_tab_local_title" = "本站"; +"mastodon_tab_public_title" = "跨站"; +"home_tab_featured_title" = "精选"; +"home_tab_bookmarks_title" = "书签"; +"home_tab_favorite_title" = "收藏"; +"home_tab_list_title" = "列表"; +"home_tab_feeds_title" = "订阅源"; +"dm_list_title" = "私信"; +"rss_title" = "订阅"; +"social_title" = "社交"; +"antenna_title" = "天线"; +"mixed_timeline_title" = "混合时间轴"; +"liked_tab_title" = "喜欢"; +"all_rss_feeds_title" = "所有订阅"; +"posts_title" = "帖子"; +"posts_with_replies_title" = "帖子 & 回复"; +"media_title" = "媒体"; +"channel_title" = "频道"; +"tab_settings_default" = "默认"; +"login_button" = "登录"; +"verify_button" = "验证"; +"cancel_button" = "取消"; +"ok_button" = "确定"; +"delete_button" = "删除"; +"service_select_next_button" = "下一步"; +"bluesky_login_username_hint" = "用户名"; +"bluesky_login_password_hint" = "密码"; +"bluesky_login_auth_factor_token_hint" = "2FA 令牌"; +"bluesky_login_oauth_button" = "使用 OAuth 登录"; +"bluesky_login_use_password_button" = "使用密码"; +"nostr_login_title" = "导入 Nostr 账号"; +"nostr_login_account_hint" = "npub、nsec、hex 密钥、bunker:// 或 nostrconnect://"; +"nostr_login_amber_button" = "连接 Amber"; +"nostr_login_qr_button" = "二维码连接"; +"misskey_channel_tab_following" = "关注中"; +"pixiv_ranking_week_title" = "每周排名"; +"pixiv_ranking_month_title" = "每月排名"; +"pixiv_ranking_day_male_title" = "男性排名"; +"pixiv_ranking_day_female_title" = "女性排名"; +"pixiv_ranking_week_original_title" = "原创排名"; +"pixiv_ranking_week_rookie_title" = "新人排名"; +"pixiv_ranking_day_manga_title" = "漫画排名"; +"illustrations_title" = "插画"; +"manga_title" = "漫画"; +"macos_settings_section_account" = "账号"; +"macos_settings_section_appearance" = "外观"; +"macos_settings_section_content" = "内容"; +"macos_settings_section_intelligence" = "AI 与翻译"; +"macos_settings_section_app" = "应用"; +"macos_action_clear" = "清理"; +"macos_action_open" = "打开"; +"macos_action_export" = "导出"; +"macos_action_import" = "导入"; +"macos_action_edit" = "编辑"; +"macos_action_run" = "运行"; +"macos_action_save" = "保存"; +"macos_action_move_up" = "上移"; +"macos_action_move_down" = "下移"; +"account_management_title" = "账号"; +"account_management_description" = "管理已登录账号、当前账号和账号排序。"; +"appearance_description" = "主题、布局、显示和媒体设置。"; +"appearance_theme_group_title" = "主题"; +"appearance_theme_group_subtitle" = "调整应用主题和头像形状。"; +"appearance_layout_group_title" = "布局"; +"appearance_layout_group_subtitle" = "控制时间线布局和帖子操作位置。"; +"appearance_display_group_title" = "显示"; +"appearance_display_group_subtitle" = "调整时间、平台标识、链接预览和浏览器行为。"; +"appearance_media_group_title" = "媒体"; +"appearance_media_group_subtitle" = "控制媒体显示、敏感内容、内容警告和自动播放。"; +"appearance_theme" = "主题"; +"appearance_theme_description" = "选择 Flare 如何跟随系统外观。"; +"appearance_theme_system" = "跟随系统"; +"appearance_theme_light" = "浅色"; +"appearance_theme_dark" = "深色"; +"appearance_avatar_shape" = "头像形状"; +"appearance_avatar_shape_description" = "选择账号头像的显示形状。"; +"appearance_avatar_shape_circle" = "圆形"; +"appearance_avatar_shape_square" = "方形"; +"appearance_timeline_display_mode" = "时间线显示模式"; +"appearance_timeline_display_mode_description" = "选择默认时间线呈现方式。"; +"appearance_timeline_display_mode_card" = "卡片"; +"appearance_timeline_display_mode_plain" = "朴素"; +"appearance_timeline_display_mode_gallery" = "画廊"; +"appearance_show_bottom_bar_labels" = "显示标签文字"; +"appearance_show_bottom_bar_labels_description" = "在主导航标签上显示文字。"; +"appearance_deck_mode" = "Deck 模式"; +"appearance_deck_mode_description" = "可用时使用多列 Deck 展示。"; +"appearance_fullWidthPost" = "帖子全宽"; +"appearance_fullWidthPost_description" = "让帖子内容使用整行宽度。"; +"appearance_post_action_style" = "帖子操作样式"; +"appearance_post_action_style_description" = "选择回复、转发、喜欢和分享按钮的排列方式。"; +"appearance_post_action_style_hidden" = "隐藏"; +"appearance_post_action_style_left_aligned" = "左对齐"; +"appearance_post_action_style_right_aligned" = "右对齐"; +"appearance_post_action_style_stretch" = "拉伸"; +"appearance_show_numbers" = "显示数量"; +"appearance_show_numbers_description" = "在帖子操作旁显示数量。"; +"appearance_absolute_timestamp" = "绝对时间"; +"appearance_absolute_timestamp_description" = "显示准确时间而不是相对时间。"; +"appearance_show_platform_logo" = "显示平台标识"; +"appearance_show_platform_logo_description" = "在帖子上显示来源平台标识。"; +"appearance_show_link_preview" = "显示链接预览"; +"appearance_show_link_preview_description" = "为链接显示预览卡片。"; +"appearance_compat_link_preview" = "兼容链接预览"; +"appearance_compat_link_preview_description" = "使用兼容性更好的预览布局。"; +"appearance_in_app_browser" = "应用内打开链接"; +"appearance_in_app_browser_description" = "在 Flare 内打开支持的链接。"; +"appearance_show_media" = "显示媒体"; +"appearance_show_media_description" = "在时间线中显示图片和视频。"; +"appearance_expand_media_size" = "放大媒体尺寸"; +"appearance_expand_media_size_description" = "尽可能使用更大的媒体预览。"; +"appearance_show_sensitive_content" = "显示敏感内容"; +"appearance_show_sensitive_content_description" = "默认显示标记为敏感的媒体。"; +"appearance_expand_content_warning" = "展开内容警告"; +"appearance_expand_content_warning_description" = "默认显示内容警告后的内容。"; +"appearance_video_autoplay" = "视频自动播放"; +"appearance_video_autoplay_description" = "选择视频自动播放条件。"; +"appearance_video_autoplay_never" = "永不"; +"appearance_video_autoplay_wifi" = "Wi-Fi"; +"appearance_video_autoplay_always" = "总是"; +"system_settings_title" = "系统设置"; +"system_settings_description" = "打开 macOS 系统设置,管理权限和系统级选项。"; +"local_filter_title" = "本地过滤"; +"local_filter_description" = "在本地隐藏匹配内容。"; +"local_filter_edit_title" = "添加过滤"; +"local_filter_edit" = "编辑"; +"local_filter_delete" = "删除"; +"local_filter_keyword_header" = "关键词"; +"local_filter_keyword_placeholder" = "关键词或正则表达式"; +"local_filter_scope_header" = "范围"; +"local_filter_timeline" = "时间线"; +"local_filter_notification" = "通知"; +"local_filter_search" = "搜索"; +"local_filter_regex" = "正则表达式"; +"storage_title" = "存储"; +"storage_description" = "清理缓存、查看日志、导入或导出数据。"; +"storage_clear_image_cache" = "清理图片缓存"; +"storage_clear_image_cache_desc" = "移除 URLCache、Kingfisher 和 WebKit 中的图片缓存。"; +"storage_clear_image_cache_confirm" = "要清理图片缓存吗?"; +"storage_clear_database_cache" = "清理数据库缓存"; +"storage_clear_database_cache_confirm" = "要清理数据库缓存吗?"; +"storage_view_app_log" = "查看应用日志"; +"storage_view_app_log_desc" = "查看已记录的网络和应用日志消息。"; +"settings_storage_export_data" = "导出数据"; +"settings_storage_export_data_desc" = "将应用数据和设置导出为 JSON。"; +"settings_storage_import_data" = "导入数据"; +"settings_storage_import_data_desc" = "导入之前导出的 JSON 文件。"; +"import_confirmation_title" = "导入数据?"; +"import_confirmation_message" = "导入会用所选文件替换本地数据和设置。"; +"import_completed" = "导入完成"; +"import_error" = "导入失败"; +"export_error" = "导出失败"; +"save_completed" = "已保存"; +"save_error" = "保存失败"; +"app_log" = "应用日志"; +"app_log_network_toggle" = "记录网络日志"; +"ai_config_title" = "AI"; +"ai_config_description" = "配置 AI 服务、模型、摘要和洞察。"; +"ai_config_post_insight" = "帖子洞察"; +"ai_config_post_insight_description" = "启用 AI 帖子分析。"; +"ai_config_summarize" = "摘要"; +"ai_config_pre_translate" = "预翻译"; +"ai_config_pre_translate_description" = "显示前自动翻译符合条件的内容。"; +"settings_translation_title" = "翻译"; +"settings_translation_description" = "配置翻译服务和自动翻译行为。"; +"settings_translation_test_description" = "使用当前服务设置运行示例。"; +"about_title" = "关于"; +"about_description" = "版本、项目链接和社区资源。"; +"settings_about_description" = "多服务社交客户端。"; +"settings_about_source_code" = "源代码"; +"settings_about_telegram" = "Telegram"; +"settings_about_telegram_description" = "加入 Telegram 社区。"; +"settings_about_discord" = "Discord"; +"settings_about_discord_description" = "加入 Discord 社区。"; +"settings_about_localization" = "本地化"; +"settings_about_localization_description" = "帮助翻译 Flare。"; +"settings_privacy_policy" = "隐私政策"; +"logout_title" = "退出登录"; +"list_empty_title" = "没有项目"; diff --git a/appleApp/macos/zh-Hant.lproj/Localizable.strings b/appleApp/macos/zh-Hant.lproj/Localizable.strings new file mode 100644 index 0000000000..f38d1df0e0 --- /dev/null +++ b/appleApp/macos/zh-Hant.lproj/Localizable.strings @@ -0,0 +1,208 @@ +"macos_sidebar_timelines" = "時間軸"; +"macos_sidebar_accounts" = "帳號"; +"macos_sidebar_library" = "資料庫"; +"macos_account_title" = "帳號"; +"macos_account_unavailable" = "不可用"; +"macos_account_add" = "新增帳號"; +"macos_loading" = "載入中"; +"macos_placeholder_account" = "帳號切換和登入入口會顯示在這裡。"; +"macos_placeholder_home" = "時間軸內容會顯示在這裡。"; +"macos_placeholder_discover" = "搜尋和探索內容會顯示在這裡。"; +"macos_placeholder_notifications" = "通知內容會顯示在這裡。"; +"macos_placeholder_compose" = "發布編輯器會顯示在這裡。"; +"macos_placeholder_drafts" = "草稿內容會顯示在這裡。"; +"macos_placeholder_rss" = "RSS 訂閱源管理會顯示在這裡。"; +"macos_placeholder_history" = "本地歷史內容會顯示在這裡。"; +"macos_placeholder_agent_history" = "Agent 對話歷史會顯示在這裡。"; +"macos_placeholder_settings" = "應用程式設定會顯示在這裡。"; +"macos_placeholder_secondary_tab" = "帳號快捷入口內容會顯示在這裡。"; +"search" = "搜尋"; +"home_compose" = "發布"; +"draft_box_title" = "草稿箱"; +"home_tab_home_title" = "首頁"; +"home_tab_notifications_title" = "通知"; +"home_tab_discover_title" = "探索"; +"home_tab_me_title" = "我"; +"settings_title" = "設定"; +"settings_rss_management_title" = "訂閱管理"; +"settings_agent_history_title" = "Flare AI"; +"local_history_title" = "本地歷史"; +"mastodon_tab_local_title" = "本站"; +"mastodon_tab_public_title" = "跨站"; +"home_tab_featured_title" = "精選"; +"home_tab_bookmarks_title" = "書籤"; +"home_tab_favorite_title" = "收藏"; +"home_tab_list_title" = "列表"; +"home_tab_feeds_title" = "訂閱源"; +"dm_list_title" = "私訊"; +"rss_title" = "訂閱"; +"social_title" = "社群"; +"antenna_title" = "天線"; +"mixed_timeline_title" = "混合時間軸"; +"liked_tab_title" = "喜歡"; +"all_rss_feeds_title" = "所有訂閱"; +"posts_title" = "貼文"; +"posts_with_replies_title" = "貼文和回覆"; +"media_title" = "媒體"; +"channel_title" = "頻道"; +"tab_settings_default" = "預設"; +"login_button" = "登入"; +"verify_button" = "驗證"; +"cancel_button" = "取消"; +"ok_button" = "確定"; +"delete_button" = "刪除"; +"service_select_next_button" = "下一步"; +"bluesky_login_username_hint" = "使用者名稱"; +"bluesky_login_password_hint" = "密碼"; +"bluesky_login_auth_factor_token_hint" = "2FA 權杖"; +"bluesky_login_oauth_button" = "使用 OAuth 登入"; +"bluesky_login_use_password_button" = "使用密碼"; +"nostr_login_title" = "匯入 Nostr 帳號"; +"nostr_login_account_hint" = "npub、nsec、hex 密鑰、bunker:// 或 nostrconnect://"; +"nostr_login_amber_button" = "連接 Amber"; +"nostr_login_qr_button" = "二維碼連接"; +"misskey_channel_tab_following" = "關注中"; +"pixiv_ranking_week_title" = "週排名"; +"pixiv_ranking_month_title" = "月排名"; +"pixiv_ranking_day_male_title" = "男性排名"; +"pixiv_ranking_day_female_title" = "女性排名"; +"pixiv_ranking_week_original_title" = "原創排名"; +"pixiv_ranking_week_rookie_title" = "新人排名"; +"pixiv_ranking_day_manga_title" = "漫畫排名"; +"illustrations_title" = "插畫"; +"manga_title" = "漫畫"; +"macos_settings_section_account" = "帳號"; +"macos_settings_section_appearance" = "外觀"; +"macos_settings_section_content" = "內容"; +"macos_settings_section_intelligence" = "AI 與翻譯"; +"macos_settings_section_app" = "應用程式"; +"macos_action_clear" = "清理"; +"macos_action_open" = "開啟"; +"macos_action_export" = "匯出"; +"macos_action_import" = "匯入"; +"macos_action_edit" = "編輯"; +"macos_action_run" = "執行"; +"macos_action_save" = "儲存"; +"macos_action_move_up" = "上移"; +"macos_action_move_down" = "下移"; +"account_management_title" = "帳號"; +"account_management_description" = "管理已登入帳號、目前帳號和帳號排序。"; +"appearance_description" = "主題、版面、顯示和媒體設定。"; +"appearance_theme_group_title" = "主題"; +"appearance_theme_group_subtitle" = "調整應用程式主題和頭像形狀。"; +"appearance_layout_group_title" = "版面"; +"appearance_layout_group_subtitle" = "控制時間軸版面和貼文操作位置。"; +"appearance_display_group_title" = "顯示"; +"appearance_display_group_subtitle" = "調整時間、平台標識、連結預覽和瀏覽器行為。"; +"appearance_media_group_title" = "媒體"; +"appearance_media_group_subtitle" = "控制媒體顯示、敏感內容、內容警告和自動播放。"; +"appearance_theme" = "主題"; +"appearance_theme_description" = "選擇 Flare 如何跟隨系統外觀。"; +"appearance_theme_system" = "跟隨系統"; +"appearance_theme_light" = "淺色"; +"appearance_theme_dark" = "深色"; +"appearance_avatar_shape" = "頭像形狀"; +"appearance_avatar_shape_description" = "選擇帳號頭像的顯示形狀。"; +"appearance_avatar_shape_circle" = "圓形"; +"appearance_avatar_shape_square" = "方形"; +"appearance_timeline_display_mode" = "時間軸顯示模式"; +"appearance_timeline_display_mode_description" = "選擇預設時間軸呈現方式。"; +"appearance_timeline_display_mode_card" = "卡片"; +"appearance_timeline_display_mode_plain" = "樸素"; +"appearance_timeline_display_mode_gallery" = "圖庫"; +"appearance_show_bottom_bar_labels" = "顯示標籤文字"; +"appearance_show_bottom_bar_labels_description" = "在主導覽標籤上顯示文字。"; +"appearance_deck_mode" = "Deck 模式"; +"appearance_deck_mode_description" = "可用時使用多欄 Deck 顯示。"; +"appearance_fullWidthPost" = "貼文全寬"; +"appearance_fullWidthPost_description" = "讓貼文內容使用整列寬度。"; +"appearance_post_action_style" = "貼文操作樣式"; +"appearance_post_action_style_description" = "選擇回覆、轉發、喜歡和分享按鈕的排列方式。"; +"appearance_post_action_style_hidden" = "隱藏"; +"appearance_post_action_style_left_aligned" = "靠左"; +"appearance_post_action_style_right_aligned" = "靠右"; +"appearance_post_action_style_stretch" = "拉伸"; +"appearance_show_numbers" = "顯示數量"; +"appearance_show_numbers_description" = "在貼文操作旁顯示數量。"; +"appearance_absolute_timestamp" = "絕對時間"; +"appearance_absolute_timestamp_description" = "顯示準確時間而不是相對時間。"; +"appearance_show_platform_logo" = "顯示平台標識"; +"appearance_show_platform_logo_description" = "在貼文上顯示來源平台標識。"; +"appearance_show_link_preview" = "顯示連結預覽"; +"appearance_show_link_preview_description" = "為連結顯示預覽卡片。"; +"appearance_compat_link_preview" = "相容連結預覽"; +"appearance_compat_link_preview_description" = "使用相容性更好的預覽版面。"; +"appearance_in_app_browser" = "應用程式內開啟連結"; +"appearance_in_app_browser_description" = "在 Flare 內開啟支援的連結。"; +"appearance_show_media" = "顯示媒體"; +"appearance_show_media_description" = "在時間軸中顯示圖片和影片。"; +"appearance_expand_media_size" = "放大媒體尺寸"; +"appearance_expand_media_size_description" = "盡可能使用更大的媒體預覽。"; +"appearance_show_sensitive_content" = "顯示敏感內容"; +"appearance_show_sensitive_content_description" = "預設顯示標記為敏感的媒體。"; +"appearance_expand_content_warning" = "展開內容警告"; +"appearance_expand_content_warning_description" = "預設顯示內容警告後的內容。"; +"appearance_video_autoplay" = "影片自動播放"; +"appearance_video_autoplay_description" = "選擇影片自動播放條件。"; +"appearance_video_autoplay_never" = "永不"; +"appearance_video_autoplay_wifi" = "Wi-Fi"; +"appearance_video_autoplay_always" = "總是"; +"system_settings_title" = "系統設定"; +"system_settings_description" = "開啟 macOS 系統設定,管理權限和系統級選項。"; +"local_filter_title" = "本地過濾"; +"local_filter_description" = "在本地隱藏符合條件的內容。"; +"local_filter_edit_title" = "新增過濾"; +"local_filter_edit" = "編輯"; +"local_filter_delete" = "刪除"; +"local_filter_keyword_header" = "關鍵字"; +"local_filter_keyword_placeholder" = "關鍵字或正規表示式"; +"local_filter_scope_header" = "範圍"; +"local_filter_timeline" = "時間軸"; +"local_filter_notification" = "通知"; +"local_filter_search" = "搜尋"; +"local_filter_regex" = "正規表示式"; +"storage_title" = "儲存空間"; +"storage_description" = "清理快取、查看日誌、匯入或匯出資料。"; +"storage_clear_image_cache" = "清理圖片快取"; +"storage_clear_image_cache_desc" = "移除 URLCache、Kingfisher 和 WebKit 中的圖片快取。"; +"storage_clear_image_cache_confirm" = "要清理圖片快取嗎?"; +"storage_clear_database_cache" = "清理資料庫快取"; +"storage_clear_database_cache_confirm" = "要清理資料庫快取嗎?"; +"storage_view_app_log" = "查看應用程式日誌"; +"storage_view_app_log_desc" = "查看已記錄的網路和應用程式日誌訊息。"; +"settings_storage_export_data" = "匯出資料"; +"settings_storage_export_data_desc" = "將應用程式資料和設定匯出為 JSON。"; +"settings_storage_import_data" = "匯入資料"; +"settings_storage_import_data_desc" = "匯入之前匯出的 JSON 檔案。"; +"import_confirmation_title" = "匯入資料?"; +"import_confirmation_message" = "匯入會用所選檔案取代本地資料和設定。"; +"import_completed" = "匯入完成"; +"import_error" = "匯入失敗"; +"export_error" = "匯出失敗"; +"save_completed" = "已儲存"; +"save_error" = "儲存失敗"; +"app_log" = "應用程式日誌"; +"app_log_network_toggle" = "記錄網路日誌"; +"ai_config_title" = "AI"; +"ai_config_description" = "配置 AI 服務、模型、摘要和洞察。"; +"ai_config_post_insight" = "貼文洞察"; +"ai_config_post_insight_description" = "啟用 AI 貼文分析。"; +"ai_config_summarize" = "摘要"; +"ai_config_pre_translate" = "預翻譯"; +"ai_config_pre_translate_description" = "顯示前自動翻譯符合條件的內容。"; +"settings_translation_title" = "翻譯"; +"settings_translation_description" = "配置翻譯服務和自動翻譯行為。"; +"settings_translation_test_description" = "使用目前服務設定執行範例。"; +"about_title" = "關於"; +"about_description" = "版本、專案連結和社群資源。"; +"settings_about_description" = "多服務社群用戶端。"; +"settings_about_source_code" = "原始碼"; +"settings_about_telegram" = "Telegram"; +"settings_about_telegram_description" = "加入 Telegram 社群。"; +"settings_about_discord" = "Discord"; +"settings_about_discord_description" = "加入 Discord 社群。"; +"settings_about_localization" = "本地化"; +"settings_about_localization_description" = "幫助翻譯 Flare。"; +"settings_privacy_policy" = "隱私權政策"; +"logout_title" = "登出"; +"list_empty_title" = "沒有項目"; diff --git a/appleApp/project.yml b/appleApp/project.yml new file mode 100644 index 0000000000..413b74ddc7 --- /dev/null +++ b/appleApp/project.yml @@ -0,0 +1,260 @@ +name: Flare + +options: + minimumXcodeGenVersion: 2.45.4 + developmentLanguage: en + defaultConfig: Release + deploymentTarget: + iOS: "17.0" + macOS: "14.0" + +configs: + Debug: debug + Release: release + +settingGroups: + appVersion: + CURRENT_PROJECT_VERSION: 1600 + MARKETING_VERSION: 1.6.0 + appleApp: + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS: YES + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME: AccentColor + CODE_SIGN_STYLE: Automatic + ENABLE_PREVIEWS: YES + ENABLE_USER_SCRIPT_SANDBOXING: NO + # FRAMEWORK_SEARCH_PATHS: + # - "$(inherited)" + # - "$(SRCROOT)/../apple-shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)" + PRODUCT_BUNDLE_IDENTIFIER: dev.dimension.flare + PRODUCT_NAME: Flare + +settings: + base: + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS: YES + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED: YES + DEAD_CODE_STRIPPING: YES + DEVELOPMENT_TEAM: 7LFDZ96332 + "EXCLUDED_ARCHS[sdk=macosx*]": x86_64 + LOCALIZATION_PREFERS_STRING_CATALOGS: YES + STRING_CATALOG_GENERATE_SYMBOLS: YES + SWIFT_APPROACHABLE_CONCURRENCY: YES + SWIFT_DEFAULT_ACTOR_ISOLATION: MainActor + SWIFT_EMIT_LOC_STRINGS: YES + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY: YES + SWIFT_VERSION: "6.3" + configs: + Release: + VALIDATE_PRODUCT: YES + +packages: + AppleFontAwesome: + url: https://github.com/Tlaster/AppleFontAwesome + branch: main + ChatLayout: + url: https://github.com/ekazaev/ChatLayout + exactVersion: 2.4.2 + CHTCollectionViewWaterfallLayout: + url: https://github.com/Tlaster/CHTCollectionViewWaterfallLayout + branch: master + Drops: + url: https://github.com/omaralbeik/Drops.git + exactVersion: 1.7.0 + GSPlayer: + url: https://github.com/wxxsw/GSPlayer + from: 0.2.30 + iOS-Backports: + url: https://github.com/superwall/iOS-Backports + exactVersion: 1.0.5 + Kingfisher: + url: https://github.com/onevcat/Kingfisher.git + exactVersion: 8.8.1 + LazyPager: + url: https://github.com/gh123man/LazyPager + exactVersion: 1.2.1 + SwiftLintPlugins: + url: https://github.com/SimplyDanny/SwiftLintPlugins + exactVersion: 0.62.2 + SwiftUI-Flow: + url: https://github.com/tevelee/SwiftUI-Flow + exactVersion: 3.1.1 + swiftui-introspect: + url: https://github.com/siteline/swiftui-introspect + exactVersion: 27.0.0-beta.1 + VideoPlayer: + url: https://github.com/wxxsw/VideoPlayer + exactVersion: 1.2.5 + +targets: + iOS: + type: application + platform: iOS + sources: + - path: ios + excludes: + - .DS_Store + - Assets.xcassets/.DS_Store + - Info.plist + - AppIcon_*.icon + dependencies: + - package: AppleFontAwesome + - package: Kingfisher + - package: LazyPager + - package: SwiftUI-Flow + product: Flow + - package: swiftui-introspect + product: SwiftUIIntrospect + - package: Drops + - package: iOS-Backports + product: SwiftUIBackports + - package: VideoPlayer + - package: CHTCollectionViewWaterfallLayout + - package: GSPlayer + - package: ChatLayout + - target: FlareAppleCore + embed: true + destinationFilters: [iOS] + - target: FlareAppleUI + embed: true + destinationFilters: [iOS] + preBuildScripts: + - name: Compile Kotlin + basedOnDependencyAnalysis: false + script: | + cd "$SRCROOT/.." + ./gradlew :apple-shared:embedAndSignAppleFrameworkForXcode + settings: + groups: + - appVersion + - appleApp + base: + ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES: "AppIcon_black AppIcon_blue AppIcon_cyan AppIcon_light_blue AppIcon_orange AppIcon_red AppIcon_teal AppIcon_white AppIcon_yellow" + ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS: NO + INFOPLIST_FILE: ios/Info.plist + + macOS: + type: application + platform: macOS + sources: + - path: macos + excludes: + - Info.plist + dependencies: + - package: AppleFontAwesome + - package: CHTCollectionViewWaterfallLayout + - package: Kingfisher + - package: swiftui-introspect + product: SwiftUIIntrospect + - target: FlareAppleCore + embed: true + destinationFilters: [macOS] + - target: FlareAppleUI + embed: true + destinationFilters: [macOS] + preBuildScripts: + - name: Compile Kotlin + basedOnDependencyAnalysis: false + script: | + cd "$SRCROOT/.." + ./gradlew :apple-shared:embedAndSignAppleFrameworkForXcode + settings: + groups: + - appVersion + - appleApp + base: + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED: YES + CODE_SIGN_ENTITLEMENTS: macos/Flare.entitlements + DEAD_CODE_STRIPPING: YES + ENABLE_APP_SANDBOX: YES + ENABLE_HARDENED_RUNTIME: YES + ENABLE_OUTGOING_NETWORK_CONNECTIONS: YES + ENABLE_USER_SELECTED_FILES: readonly + INFOPLIST_FILE: macos/Info.plist + OTHER_LDFLAGS: "$(inherited) -lsqlite3" + REGISTER_APP_GROUPS: YES + + FlareAppleCore: + type: framework + platform: auto + supportedDestinations: [iOS, macOS] + sources: + - path: Shared/FlareAppleCore/Sources/FlareAppleCore + dependencies: + - package: AppleFontAwesome + preBuildScripts: + - name: Compile Kotlin + basedOnDependencyAnalysis: false + script: | + cd "$SRCROOT/.." + ./gradlew :apple-shared:embedAndSignAppleFrameworkForXcode + settings: + base: + CLANG_ENABLE_MODULE_VERIFIER: YES + DEAD_CODE_STRIPPING: YES + # FRAMEWORK_SEARCH_PATHS: + # - "$(inherited)" + # - "$(SRCROOT)/../apple-shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)" + GENERATE_INFOPLIST_FILE: YES + OTHER_LDFLAGS: "$(inherited) -lsqlite3" + PRODUCT_BUNDLE_IDENTIFIER: dev.dimension.flare.applecore + + FlareAppleUI: + type: framework + platform: auto + supportedDestinations: [iOS, macOS] + sources: + - path: Shared/FlareAppleCore/Sources/FlareAppleUI + dependencies: + - package: AppleFontAwesome + - package: Kingfisher + - package: iOS-Backports + product: SwiftUIBackports + - package: VideoPlayer + destinationFilters: [iOS] + - target: FlareAppleCore + preBuildScripts: + - name: Compile Kotlin + basedOnDependencyAnalysis: false + script: | + cd "$SRCROOT/.." + ./gradlew :apple-shared:embedAndSignAppleFrameworkForXcode + settings: + base: + CLANG_ENABLE_MODULE_VERIFIER: YES + DEAD_CODE_STRIPPING: YES + # FRAMEWORK_SEARCH_PATHS: + # - "$(inherited)" + # - "$(SRCROOT)/../apple-shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)" + GENERATE_INFOPLIST_FILE: YES + OTHER_LDFLAGS: "$(inherited) -lsqlite3" + PRODUCT_BUNDLE_IDENTIFIER: dev.dimension.flare.appleui + +schemes: + iOS: + build: + targets: + iOS: all + run: + config: Debug + test: + config: Debug + profile: + config: Release + analyze: + config: Debug + archive: + config: Release + + macOS: + build: + targets: + macOS: all + run: + config: Debug + test: + config: Debug + profile: + config: Release + analyze: + config: Debug + archive: + config: Release diff --git a/crowdin.yml b/crowdin.yml index ca30c89df5..6c6f73760e 100644 --- a/crowdin.yml +++ b/crowdin.yml @@ -17,8 +17,8 @@ files: "excluded_target_languages": ["fr", "it"], }, { - "source": "iosApp/flare/Localizable.xcstrings", - "translation": "iosApp/flare/Localizable.xcstrings", + "source": "appleApp/ios/Localizable.xcstrings", + "translation": "appleApp/ios/Localizable.xcstrings", "multilingual": true, }, ] diff --git a/desktopApp/build.gradle.kts b/desktopApp/build.gradle.kts index 295cd1f04b..5fc917ccd5 100644 --- a/desktopApp/build.gradle.kts +++ b/desktopApp/build.gradle.kts @@ -203,7 +203,7 @@ $localizationsXml } iconFile.set(project.file("resources/ic_launcher.icns")) - layeredIconDir.set(rootProject.file("iosApp/flare/AppIcon.icon")) + layeredIconDir.set(rootProject.file("appleApp/ios/AppIcon.icon")) } windows { iconFile.set(project.file("resources/ic_launcher.ico")) diff --git a/iosApp/flare/Assets.xcassets/fa-at.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-at.symbolset/Contents.json deleted file mode 100644 index 5f0b31db8d..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-at.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-at.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-at.symbolset/fa-at.svg b/iosApp/flare/Assets.xcassets/fa-at.symbolset/fa-at.svg deleted file mode 100644 index 098384db1c..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-at.symbolset/fa-at.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-bell.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-bell.symbolset/Contents.json deleted file mode 100644 index ebe005eeea..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-bell.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-bell.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-bell.symbolset/fa-bell.svg b/iosApp/flare/Assets.xcassets/fa-bell.symbolset/fa-bell.svg deleted file mode 100644 index 6c3117f051..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-bell.symbolset/fa-bell.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-bluesky.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-bluesky.symbolset/Contents.json deleted file mode 100644 index 22a683c990..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-bluesky.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-bluesky.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-bluesky.symbolset/fa-bluesky.svg b/iosApp/flare/Assets.xcassets/fa-bluesky.symbolset/fa-bluesky.svg deleted file mode 100644 index 417c482671..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-bluesky.symbolset/fa-bluesky.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-book-bookmark.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-book-bookmark.symbolset/Contents.json deleted file mode 100644 index eaa15bcb8b..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-book-bookmark.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-book-bookmark.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-book-bookmark.symbolset/fa-book-bookmark.svg b/iosApp/flare/Assets.xcassets/fa-book-bookmark.symbolset/fa-book-bookmark.svg deleted file mode 100644 index 702aeb8839..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-book-bookmark.symbolset/fa-book-bookmark.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-bookmark.fill.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-bookmark.fill.symbolset/Contents.json deleted file mode 100644 index 049f70244a..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-bookmark.fill.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-bookmark.fill.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-bookmark.fill.symbolset/fa-bookmark.fill.svg b/iosApp/flare/Assets.xcassets/fa-bookmark.fill.symbolset/fa-bookmark.fill.svg deleted file mode 100644 index 532476d73d..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-bookmark.fill.symbolset/fa-bookmark.fill.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-bookmark.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-bookmark.symbolset/Contents.json deleted file mode 100644 index 79d04f2cbb..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-bookmark.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-bookmark.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-bookmark.symbolset/fa-bookmark.svg b/iosApp/flare/Assets.xcassets/fa-bookmark.symbolset/fa-bookmark.svg deleted file mode 100644 index f126d34fab..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-bookmark.symbolset/fa-bookmark.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-cat.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-cat.symbolset/Contents.json deleted file mode 100644 index d6123cabbe..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-cat.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-cat.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-cat.symbolset/fa-cat.svg b/iosApp/flare/Assets.xcassets/fa-cat.symbolset/fa-cat.svg deleted file mode 100644 index 0f28a5fb25..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-cat.symbolset/fa-cat.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-check.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-check.symbolset/Contents.json deleted file mode 100644 index 8feeade3d8..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-check.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-check.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-check.symbolset/fa-check.svg b/iosApp/flare/Assets.xcassets/fa-check.symbolset/fa-check.svg deleted file mode 100644 index 905f8ea1ce..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-check.symbolset/fa-check.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-chevron-down.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-chevron-down.symbolset/Contents.json deleted file mode 100644 index 2636351bc1..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-chevron-down.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-chevron-down.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-chevron-down.symbolset/fa-chevron-down.svg b/iosApp/flare/Assets.xcassets/fa-chevron-down.symbolset/fa-chevron-down.svg deleted file mode 100644 index ac165f37ca..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-chevron-down.symbolset/fa-chevron-down.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-circle-check.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-circle-check.symbolset/Contents.json deleted file mode 100644 index e47df24dde..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-circle-check.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-circle-check.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-circle-check.symbolset/fa-circle-check.svg b/iosApp/flare/Assets.xcassets/fa-circle-check.symbolset/fa-circle-check.svg deleted file mode 100644 index 555d082783..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-circle-check.symbolset/fa-circle-check.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-circle-chevron-down.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-circle-chevron-down.symbolset/Contents.json deleted file mode 100644 index d9dcb050d3..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-circle-chevron-down.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-circle-chevron-down.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-circle-chevron-down.symbolset/fa-circle-chevron-down.svg b/iosApp/flare/Assets.xcassets/fa-circle-chevron-down.symbolset/fa-circle-chevron-down.svg deleted file mode 100644 index 23f4a7c623..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-circle-chevron-down.symbolset/fa-circle-chevron-down.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-circle-exclamation.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-circle-exclamation.symbolset/Contents.json deleted file mode 100644 index d0fa371251..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-circle-exclamation.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-circle-exclamation.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-circle-exclamation.symbolset/fa-circle-exclamation.svg b/iosApp/flare/Assets.xcassets/fa-circle-exclamation.symbolset/fa-circle-exclamation.svg deleted file mode 100644 index 88080ded08..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-circle-exclamation.symbolset/fa-circle-exclamation.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-circle-info.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-circle-info.symbolset/Contents.json deleted file mode 100644 index 9bbfc9c8a3..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-circle-info.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-circle-info.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-circle-info.symbolset/fa-circle-info.svg b/iosApp/flare/Assets.xcassets/fa-circle-info.symbolset/fa-circle-info.svg deleted file mode 100644 index a96a393937..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-circle-info.symbolset/fa-circle-info.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-circle-play.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-circle-play.symbolset/Contents.json deleted file mode 100644 index 2cb514b634..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-circle-play.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-circle-play.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-circle-play.symbolset/fa-circle-play.svg b/iosApp/flare/Assets.xcassets/fa-circle-play.symbolset/fa-circle-play.svg deleted file mode 100644 index 792d0839f4..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-circle-play.symbolset/fa-circle-play.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-circle-user.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-circle-user.symbolset/Contents.json deleted file mode 100644 index c5c4de9278..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-circle-user.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-circle-user.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-circle-user.symbolset/fa-circle-user.svg b/iosApp/flare/Assets.xcassets/fa-circle-user.symbolset/fa-circle-user.svg deleted file mode 100644 index 4d086e8b88..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-circle-user.symbolset/fa-circle-user.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-clock-rotate-left.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-clock-rotate-left.symbolset/Contents.json deleted file mode 100644 index d2f1869c75..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-clock-rotate-left.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-clock-rotate-left.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-clock-rotate-left.symbolset/fa-clock-rotate-left.svg b/iosApp/flare/Assets.xcassets/fa-clock-rotate-left.symbolset/fa-clock-rotate-left.svg deleted file mode 100644 index dd8d361275..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-clock-rotate-left.symbolset/fa-clock-rotate-left.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-comment-dots.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-comment-dots.symbolset/Contents.json deleted file mode 100644 index 6b287c0955..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-comment-dots.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-comment-dots.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-comment-dots.symbolset/fa-comment-dots.svg b/iosApp/flare/Assets.xcassets/fa-comment-dots.symbolset/fa-comment-dots.svg deleted file mode 100644 index 56b1874d64..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-comment-dots.symbolset/fa-comment-dots.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-database.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-database.symbolset/Contents.json deleted file mode 100644 index 7736275a36..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-database.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-database.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-database.symbolset/fa-database.svg b/iosApp/flare/Assets.xcassets/fa-database.symbolset/fa-database.svg deleted file mode 100644 index 2275ed0eac..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-database.symbolset/fa-database.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-delete-left.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-delete-left.symbolset/Contents.json deleted file mode 100644 index 6ca368298e..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-delete-left.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-delete-left.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-delete-left.symbolset/fa-delete-left.svg b/iosApp/flare/Assets.xcassets/fa-delete-left.symbolset/fa-delete-left.svg deleted file mode 100644 index c1e44391f2..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-delete-left.symbolset/fa-delete-left.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-discord.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-discord.symbolset/Contents.json deleted file mode 100644 index 7f4988f444..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-discord.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-discord.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-discord.symbolset/fa-discord.svg b/iosApp/flare/Assets.xcassets/fa-discord.symbolset/fa-discord.svg deleted file mode 100644 index ef7fa5eab1..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-discord.symbolset/fa-discord.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-download.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-download.symbolset/Contents.json deleted file mode 100644 index 68f2dbd2b3..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-download.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-download.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-download.symbolset/fa-download.svg b/iosApp/flare/Assets.xcassets/fa-download.symbolset/fa-download.svg deleted file mode 100644 index 075f7f34f6..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-download.symbolset/fa-download.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-ellipsis-vertical.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-ellipsis-vertical.symbolset/Contents.json deleted file mode 100644 index 42b1ed7772..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-ellipsis-vertical.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-ellipsis-vertical.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-ellipsis-vertical.symbolset/fa-ellipsis-vertical.svg b/iosApp/flare/Assets.xcassets/fa-ellipsis-vertical.symbolset/fa-ellipsis-vertical.svg deleted file mode 100644 index e2c1ad05fe..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-ellipsis-vertical.symbolset/fa-ellipsis-vertical.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-ellipsis.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-ellipsis.symbolset/Contents.json deleted file mode 100644 index f585ef72cc..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-ellipsis.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-ellipsis.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-ellipsis.symbolset/fa-ellipsis.svg b/iosApp/flare/Assets.xcassets/fa-ellipsis.symbolset/fa-ellipsis.svg deleted file mode 100644 index 5d88133b23..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-ellipsis.symbolset/fa-ellipsis.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-envelope.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-envelope.symbolset/Contents.json deleted file mode 100644 index e4c0a715eb..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-envelope.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-envelope.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-envelope.symbolset/fa-envelope.svg b/iosApp/flare/Assets.xcassets/fa-envelope.symbolset/fa-envelope.svg deleted file mode 100644 index 2bd05d557f..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-envelope.symbolset/fa-envelope.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-eye-slash.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-eye-slash.symbolset/Contents.json deleted file mode 100644 index 8b30476e0f..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-eye-slash.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-eye-slash.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-eye-slash.symbolset/fa-eye-slash.svg b/iosApp/flare/Assets.xcassets/fa-eye-slash.symbolset/fa-eye-slash.svg deleted file mode 100644 index c322093241..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-eye-slash.symbolset/fa-eye-slash.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-eye.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-eye.symbolset/Contents.json deleted file mode 100644 index 70408bff36..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-eye.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-eye.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-eye.symbolset/fa-eye.svg b/iosApp/flare/Assets.xcassets/fa-eye.symbolset/fa-eye.svg deleted file mode 100644 index ab9eaa19dd..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-eye.symbolset/fa-eye.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-face-smile.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-face-smile.symbolset/Contents.json deleted file mode 100644 index 8030b7adf9..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-face-smile.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-face-smile.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-face-smile.symbolset/fa-face-smile.svg b/iosApp/flare/Assets.xcassets/fa-face-smile.symbolset/fa-face-smile.svg deleted file mode 100644 index e6a867ff65..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-face-smile.symbolset/fa-face-smile.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-file-export.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-file-export.symbolset/Contents.json deleted file mode 100644 index 99d679f91f..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-file-export.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-file-export.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-file-export.symbolset/fa-file-export.svg b/iosApp/flare/Assets.xcassets/fa-file-export.symbolset/fa-file-export.svg deleted file mode 100644 index c9d87bec16..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-file-export.symbolset/fa-file-export.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-file-import.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-file-import.symbolset/Contents.json deleted file mode 100644 index f8858a4d1e..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-file-import.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-file-import.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-file-import.symbolset/fa-file-import.svg b/iosApp/flare/Assets.xcassets/fa-file-import.symbolset/fa-file-import.svg deleted file mode 100644 index 4ef9608d03..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-file-import.symbolset/fa-file-import.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-filter.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-filter.symbolset/Contents.json deleted file mode 100644 index 1082ddb31a..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-filter.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-filter.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-filter.symbolset/fa-filter.svg b/iosApp/flare/Assets.xcassets/fa-filter.symbolset/fa-filter.svg deleted file mode 100644 index 82486b7e0f..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-filter.symbolset/fa-filter.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-floppy-disk.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-floppy-disk.symbolset/Contents.json deleted file mode 100644 index 8ecbc8c3e5..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-floppy-disk.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-floppy-disk.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-floppy-disk.symbolset/fa-floppy-disk.svg b/iosApp/flare/Assets.xcassets/fa-floppy-disk.symbolset/fa-floppy-disk.svg deleted file mode 100644 index 0522758eaa..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-floppy-disk.symbolset/fa-floppy-disk.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-gear.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-gear.symbolset/Contents.json deleted file mode 100644 index 8f1dcc7ed8..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-gear.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-gear.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-gear.symbolset/fa-gear.svg b/iosApp/flare/Assets.xcassets/fa-gear.symbolset/fa-gear.svg deleted file mode 100644 index ad5d127e0f..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-gear.symbolset/fa-gear.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-github.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-github.symbolset/Contents.json deleted file mode 100644 index 78b8e62454..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-github.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-github.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-github.symbolset/fa-github.svg b/iosApp/flare/Assets.xcassets/fa-github.symbolset/fa-github.svg deleted file mode 100644 index d6af429c71..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-github.symbolset/fa-github.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-globe.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-globe.symbolset/Contents.json deleted file mode 100644 index 3415054cad..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-globe.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-globe.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-globe.symbolset/fa-globe.svg b/iosApp/flare/Assets.xcassets/fa-globe.symbolset/fa-globe.svg deleted file mode 100644 index d0a5da65a9..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-globe.symbolset/fa-globe.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-heart-circle-minus.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-heart-circle-minus.symbolset/Contents.json deleted file mode 100644 index 17203bfcbf..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-heart-circle-minus.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-heart-circle-minus.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-heart-circle-minus.symbolset/fa-heart-circle-minus.svg b/iosApp/flare/Assets.xcassets/fa-heart-circle-minus.symbolset/fa-heart-circle-minus.svg deleted file mode 100644 index a643d30933..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-heart-circle-minus.symbolset/fa-heart-circle-minus.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-heart-circle-plus.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-heart-circle-plus.symbolset/Contents.json deleted file mode 100644 index 22e9f8813c..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-heart-circle-plus.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-heart-circle-plus.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-heart-circle-plus.symbolset/fa-heart-circle-plus.svg b/iosApp/flare/Assets.xcassets/fa-heart-circle-plus.symbolset/fa-heart-circle-plus.svg deleted file mode 100644 index 9ab747d522..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-heart-circle-plus.symbolset/fa-heart-circle-plus.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-heart.fill.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-heart.fill.symbolset/Contents.json deleted file mode 100644 index dd5132baa7..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-heart.fill.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-heart.fill.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-heart.fill.symbolset/fa-heart.fill.svg b/iosApp/flare/Assets.xcassets/fa-heart.fill.symbolset/fa-heart.fill.svg deleted file mode 100644 index 8ea87173d3..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-heart.fill.symbolset/fa-heart.fill.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-heart.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-heart.symbolset/Contents.json deleted file mode 100644 index f6457fa7b5..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-heart.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-heart.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-heart.symbolset/fa-heart.svg b/iosApp/flare/Assets.xcassets/fa-heart.symbolset/fa-heart.svg deleted file mode 100644 index 8e6b7af1c2..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-heart.symbolset/fa-heart.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-house.symbolset/house.svg b/iosApp/flare/Assets.xcassets/fa-house.symbolset/house.svg deleted file mode 100644 index 0a0de7b9b0..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-house.symbolset/house.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-image.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-image.symbolset/Contents.json deleted file mode 100644 index 4308d1c81f..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-image.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-image.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-image.symbolset/fa-image.svg b/iosApp/flare/Assets.xcassets/fa-image.symbolset/fa-image.svg deleted file mode 100644 index 86d8a18503..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-image.symbolset/fa-image.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-inbox.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-inbox.symbolset/Contents.json deleted file mode 100644 index ec922d7e5b..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-inbox.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-inbox.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-inbox.symbolset/fa-inbox.svg b/iosApp/flare/Assets.xcassets/fa-inbox.symbolset/fa-inbox.svg deleted file mode 100644 index 3ee6520ee6..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-inbox.symbolset/fa-inbox.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-language.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-language.symbolset/Contents.json deleted file mode 100644 index 265d4ac9c8..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-language.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-language.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-language.symbolset/fa-language.svg b/iosApp/flare/Assets.xcassets/fa-language.symbolset/fa-language.svg deleted file mode 100644 index d0edf52b60..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-language.symbolset/fa-language.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-list.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-list.symbolset/Contents.json deleted file mode 100644 index dad2f63d98..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-list.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-list.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-list.symbolset/fa-list.svg b/iosApp/flare/Assets.xcassets/fa-list.symbolset/fa-list.svg deleted file mode 100644 index 55e6ba18fe..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-list.symbolset/fa-list.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-location-dot.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-location-dot.symbolset/Contents.json deleted file mode 100644 index c84d5addc2..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-location-dot.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-location-dot.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-location-dot.symbolset/fa-location-dot.svg b/iosApp/flare/Assets.xcassets/fa-location-dot.symbolset/fa-location-dot.svg deleted file mode 100644 index 7cf3181034..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-location-dot.symbolset/fa-location-dot.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-lock-open.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-lock-open.symbolset/Contents.json deleted file mode 100644 index 11790cfe20..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-lock-open.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-lock-open.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-lock-open.symbolset/fa-lock-open.svg b/iosApp/flare/Assets.xcassets/fa-lock-open.symbolset/fa-lock-open.svg deleted file mode 100644 index b21450d82b..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-lock-open.symbolset/fa-lock-open.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-lock.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-lock.symbolset/Contents.json deleted file mode 100644 index fa72c1ba5c..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-lock.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-lock.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-lock.symbolset/fa-lock.svg b/iosApp/flare/Assets.xcassets/fa-lock.symbolset/fa-lock.svg deleted file mode 100644 index e80472caca..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-lock.symbolset/fa-lock.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-magnifying-glass.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-magnifying-glass.symbolset/Contents.json deleted file mode 100644 index e913377eae..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-magnifying-glass.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-magnifying-glass.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-magnifying-glass.symbolset/fa-magnifying-glass.svg b/iosApp/flare/Assets.xcassets/fa-magnifying-glass.symbolset/fa-magnifying-glass.svg deleted file mode 100644 index 978cfce464..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-magnifying-glass.symbolset/fa-magnifying-glass.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-mastodon.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-mastodon.symbolset/Contents.json deleted file mode 100644 index b5e9dcd2aa..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-mastodon.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-mastodon.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-mastodon.symbolset/fa-mastodon.svg b/iosApp/flare/Assets.xcassets/fa-mastodon.symbolset/fa-mastodon.svg deleted file mode 100644 index a227d571b8..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-mastodon.symbolset/fa-mastodon.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-message.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-message.symbolset/Contents.json deleted file mode 100644 index 590e2af70c..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-message.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-message.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-message.symbolset/fa-message.svg b/iosApp/flare/Assets.xcassets/fa-message.symbolset/fa-message.svg deleted file mode 100644 index 8a0374e26e..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-message.symbolset/fa-message.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-minus.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-minus.symbolset/Contents.json deleted file mode 100644 index 2552bdc56b..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-minus.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-minus.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-minus.symbolset/fa-minus.svg b/iosApp/flare/Assets.xcassets/fa-minus.symbolset/fa-minus.svg deleted file mode 100644 index bc2b1261a0..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-minus.symbolset/fa-minus.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-misskey.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-misskey.symbolset/Contents.json deleted file mode 100644 index 0efcf9908f..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-misskey.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-misskey.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-misskey.symbolset/fa-misskey.svg b/iosApp/flare/Assets.xcassets/fa-misskey.symbolset/fa-misskey.svg deleted file mode 100644 index cbc45d3b2f..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-misskey.symbolset/fa-misskey.svg +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - Small - Medium - Large - - - Ultralight - Regular - Black - Template v.3.0 - - https://wangchujiang.com/#/app - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-newspaper.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-newspaper.symbolset/Contents.json deleted file mode 100644 index 201861d8ff..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-newspaper.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-newspaper.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-newspaper.symbolset/fa-newspaper.svg b/iosApp/flare/Assets.xcassets/fa-newspaper.symbolset/fa-newspaper.svg deleted file mode 100644 index 44f7005ea3..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-newspaper.symbolset/fa-newspaper.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-nostr.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-nostr.symbolset/Contents.json deleted file mode 100644 index ef0201af9b..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-nostr.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-nostr.symbols.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-nostr.symbolset/fa-nostr.symbols.svg b/iosApp/flare/Assets.xcassets/fa-nostr.symbolset/fa-nostr.symbols.svg deleted file mode 100644 index 3c25581250..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-nostr.symbolset/fa-nostr.symbols.svg +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - Small - Medium - Large - - - Ultralight - Regular - Black - Template v.3.0 - - https://wangchujiang.com/#/app - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-palette.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-palette.symbolset/Contents.json deleted file mode 100644 index 8db9c8d413..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-palette.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-palette.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-palette.symbolset/fa-palette.svg b/iosApp/flare/Assets.xcassets/fa-palette.symbolset/fa-palette.svg deleted file mode 100644 index 990325387d..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-palette.symbolset/fa-palette.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-pause.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-pause.symbolset/Contents.json deleted file mode 100644 index 77a48c554e..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-pause.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-pause.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-pause.symbolset/fa-pause.svg b/iosApp/flare/Assets.xcassets/fa-pause.symbolset/fa-pause.svg deleted file mode 100644 index 239959ea5a..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-pause.symbolset/fa-pause.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-pen-to-square.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-pen-to-square.symbolset/Contents.json deleted file mode 100644 index 35bbe61971..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-pen-to-square.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-pen-to-square.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-pen-to-square.symbolset/fa-pen-to-square.svg b/iosApp/flare/Assets.xcassets/fa-pen-to-square.symbolset/fa-pen-to-square.svg deleted file mode 100644 index 608b5e6944..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-pen-to-square.symbolset/fa-pen-to-square.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-pen.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-pen.symbolset/Contents.json deleted file mode 100644 index 4c25baadec..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-pen.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-pen.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-pen.symbolset/fa-pen.svg b/iosApp/flare/Assets.xcassets/fa-pen.symbolset/fa-pen.svg deleted file mode 100644 index 4b9d38eced..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-pen.symbolset/fa-pen.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-photo-film.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-photo-film.symbolset/Contents.json deleted file mode 100644 index 2ec1d06773..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-photo-film.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-photo-film.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-photo-film.symbolset/fa-photo-film.svg b/iosApp/flare/Assets.xcassets/fa-photo-film.symbolset/fa-photo-film.svg deleted file mode 100644 index 536bc56484..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-photo-film.symbolset/fa-photo-film.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-pixiv.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-pixiv.symbolset/Contents.json deleted file mode 100644 index 421ea122e2..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-pixiv.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-pixiv.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-pixiv.symbolset/fa-pixiv.svg b/iosApp/flare/Assets.xcassets/fa-pixiv.symbolset/fa-pixiv.svg deleted file mode 100644 index 2a186932c2..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-pixiv.symbolset/fa-pixiv.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-play.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-play.symbolset/Contents.json deleted file mode 100644 index 6fc9be70c1..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-play.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-play.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-play.symbolset/fa-play.svg b/iosApp/flare/Assets.xcassets/fa-play.symbolset/fa-play.svg deleted file mode 100644 index 64d1519c29..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-play.symbolset/fa-play.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-plus.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-plus.symbolset/Contents.json deleted file mode 100644 index db0f06ba29..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-plus.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-plus.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-plus.symbolset/fa-plus.svg b/iosApp/flare/Assets.xcassets/fa-plus.symbolset/fa-plus.svg deleted file mode 100644 index ede164363d..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-plus.symbolset/fa-plus.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-quote-left.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-quote-left.symbolset/Contents.json deleted file mode 100644 index afcacf6697..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-quote-left.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-quote-left.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-quote-left.symbolset/fa-quote-left.svg b/iosApp/flare/Assets.xcassets/fa-quote-left.symbolset/fa-quote-left.svg deleted file mode 100644 index 1b422a2586..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-quote-left.symbolset/fa-quote-left.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-rectangle-list.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-rectangle-list.symbolset/Contents.json deleted file mode 100644 index 5c44bbcbd1..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-rectangle-list.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-rectangle-list.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-rectangle-list.symbolset/fa-rectangle-list.svg b/iosApp/flare/Assets.xcassets/fa-rectangle-list.symbolset/fa-rectangle-list.svg deleted file mode 100644 index 00bc3fe97b..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-rectangle-list.symbolset/fa-rectangle-list.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-reply.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-reply.symbolset/Contents.json deleted file mode 100644 index 750c0b857d..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-reply.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-reply.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-reply.symbolset/fa-reply.svg b/iosApp/flare/Assets.xcassets/fa-reply.symbolset/fa-reply.svg deleted file mode 100644 index a4e91e5dfe..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-reply.symbolset/fa-reply.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-retweet.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-retweet.symbolset/Contents.json deleted file mode 100644 index 8b48cfc81e..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-retweet.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-retweet.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-retweet.symbolset/fa-retweet.svg b/iosApp/flare/Assets.xcassets/fa-retweet.symbolset/fa-retweet.svg deleted file mode 100644 index fc77281662..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-retweet.symbolset/fa-retweet.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-robot.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-robot.symbolset/Contents.json deleted file mode 100644 index 829c8f88c0..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-robot.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-robot.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-robot.symbolset/fa-robot.svg b/iosApp/flare/Assets.xcassets/fa-robot.symbolset/fa-robot.svg deleted file mode 100644 index eb3eb7a0a9..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-robot.symbolset/fa-robot.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-share-nodes.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-share-nodes.symbolset/Contents.json deleted file mode 100644 index 55ad2f46bb..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-share-nodes.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-share-nodes.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-share-nodes.symbolset/fa-share-nodes.svg b/iosApp/flare/Assets.xcassets/fa-share-nodes.symbolset/fa-share-nodes.svg deleted file mode 100644 index 3b1a989965..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-share-nodes.symbolset/fa-share-nodes.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-sliders.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-sliders.symbolset/Contents.json deleted file mode 100644 index a8864ddce5..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-sliders.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-sliders.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-sliders.symbolset/fa-sliders.svg b/iosApp/flare/Assets.xcassets/fa-sliders.symbolset/fa-sliders.svg deleted file mode 100644 index c767d411ca..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-sliders.symbolset/fa-sliders.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-square-poll-horizontal.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-square-poll-horizontal.symbolset/Contents.json deleted file mode 100644 index 353029c4ae..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-square-poll-horizontal.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-square-poll-horizontal.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-square-poll-horizontal.symbolset/fa-square-poll-horizontal.svg b/iosApp/flare/Assets.xcassets/fa-square-poll-horizontal.symbolset/fa-square-poll-horizontal.svg deleted file mode 100644 index 358b3f0ef8..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-square-poll-horizontal.symbolset/fa-square-poll-horizontal.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-square-rss.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-square-rss.symbolset/Contents.json deleted file mode 100644 index 65247601a2..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-square-rss.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-square-rss.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-square-rss.symbolset/fa-square-rss.svg b/iosApp/flare/Assets.xcassets/fa-square-rss.symbolset/fa-square-rss.svg deleted file mode 100644 index 43595dc5bf..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-square-rss.symbolset/fa-square-rss.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-star-fill.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-star-fill.symbolset/Contents.json deleted file mode 100644 index b7ee412aa5..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-star-fill.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-star-fill.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-star-fill.symbolset/fa-star-fill.svg b/iosApp/flare/Assets.xcassets/fa-star-fill.symbolset/fa-star-fill.svg deleted file mode 100644 index 1b1f312f4c..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-star-fill.symbolset/fa-star-fill.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-star.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-star.symbolset/Contents.json deleted file mode 100644 index 19ce825ac4..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-star.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-star.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-star.symbolset/fa-star.svg b/iosApp/flare/Assets.xcassets/fa-star.symbolset/fa-star.svg deleted file mode 100644 index 0f2161d754..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-star.symbolset/fa-star.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-table-list.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-table-list.symbolset/Contents.json deleted file mode 100644 index e28880e7c7..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-table-list.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-table-list.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-table-list.symbolset/fa-table-list.svg b/iosApp/flare/Assets.xcassets/fa-table-list.symbolset/fa-table-list.svg deleted file mode 100644 index 236e57c707..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-table-list.symbolset/fa-table-list.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-telegram.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-telegram.symbolset/Contents.json deleted file mode 100644 index ee6b79dcd6..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-telegram.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-telegram.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-telegram.symbolset/fa-telegram.svg b/iosApp/flare/Assets.xcassets/fa-telegram.symbolset/fa-telegram.svg deleted file mode 100644 index abe254c42e..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-telegram.symbolset/fa-telegram.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-thumbtack.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-thumbtack.symbolset/Contents.json deleted file mode 100644 index 02449a16d1..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-thumbtack.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-thumbtack.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-thumbtack.symbolset/fa-thumbtack.svg b/iosApp/flare/Assets.xcassets/fa-thumbtack.symbolset/fa-thumbtack.svg deleted file mode 100644 index cc4f287e62..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-thumbtack.symbolset/fa-thumbtack.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-trash.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-trash.symbolset/Contents.json deleted file mode 100644 index 4e982f6f31..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-trash.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-trash.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-trash.symbolset/fa-trash.svg b/iosApp/flare/Assets.xcassets/fa-trash.symbolset/fa-trash.svg deleted file mode 100644 index c8e60c4666..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-trash.symbolset/fa-trash.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-tv.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-tv.symbolset/Contents.json deleted file mode 100644 index 7c8182106c..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-tv.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-tv.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-tv.symbolset/fa-tv.svg b/iosApp/flare/Assets.xcassets/fa-tv.symbolset/fa-tv.svg deleted file mode 100644 index e0177a3ee7..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-tv.symbolset/fa-tv.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-twitter.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-twitter.symbolset/Contents.json deleted file mode 100644 index 01f165e665..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-twitter.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-twitter.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-twitter.symbolset/fa-twitter.svg b/iosApp/flare/Assets.xcassets/fa-twitter.symbolset/fa-twitter.svg deleted file mode 100644 index 72e83489b2..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-twitter.symbolset/fa-twitter.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-user-plus.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-user-plus.symbolset/Contents.json deleted file mode 100644 index 5d34a9e01f..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-user-plus.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-user-plus.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-user-plus.symbolset/fa-user-plus.svg b/iosApp/flare/Assets.xcassets/fa-user-plus.symbolset/fa-user-plus.svg deleted file mode 100644 index fffc3e069d..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-user-plus.symbolset/fa-user-plus.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-user-slash.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-user-slash.symbolset/Contents.json deleted file mode 100644 index b5a26a7bdb..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-user-slash.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-user-slash.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-user-slash.symbolset/fa-user-slash.svg b/iosApp/flare/Assets.xcassets/fa-user-slash.symbolset/fa-user-slash.svg deleted file mode 100644 index e0e9b53184..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-user-slash.symbolset/fa-user-slash.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-users.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-users.symbolset/Contents.json deleted file mode 100644 index 9c23ae3b3e..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-users.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-users.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-users.symbolset/fa-users.svg b/iosApp/flare/Assets.xcassets/fa-users.symbolset/fa-users.svg deleted file mode 100644 index eafe0e7fde..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-users.symbolset/fa-users.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-volume-xmark.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-volume-xmark.symbolset/Contents.json deleted file mode 100644 index b241c87763..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-volume-xmark.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-volume-xmark.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-volume-xmark.symbolset/fa-volume-xmark.svg b/iosApp/flare/Assets.xcassets/fa-volume-xmark.symbolset/fa-volume-xmark.svg deleted file mode 100644 index 8b2f3985cd..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-volume-xmark.symbolset/fa-volume-xmark.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-weibo.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-weibo.symbolset/Contents.json deleted file mode 100644 index 9a9665a915..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-weibo.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-weibo.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-weibo.symbolset/fa-weibo.svg b/iosApp/flare/Assets.xcassets/fa-weibo.symbolset/fa-weibo.svg deleted file mode 100644 index 1f1b840df0..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-weibo.symbolset/fa-weibo.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-x-twitter.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-x-twitter.symbolset/Contents.json deleted file mode 100644 index b6e493d17d..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-x-twitter.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-x-twitter.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-x-twitter.symbolset/fa-x-twitter.svg b/iosApp/flare/Assets.xcassets/fa-x-twitter.symbolset/fa-x-twitter.svg deleted file mode 100644 index 2517359d85..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-x-twitter.symbolset/fa-x-twitter.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/fa-xmark.symbolset/Contents.json b/iosApp/flare/Assets.xcassets/fa-xmark.symbolset/Contents.json deleted file mode 100644 index 202688655e..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-xmark.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "fa-xmark.svg", - "idiom" : "universal" - } - ] -} diff --git a/iosApp/flare/Assets.xcassets/fa-xmark.symbolset/fa-xmark.svg b/iosApp/flare/Assets.xcassets/fa-xmark.symbolset/fa-xmark.svg deleted file mode 100644 index 72cb5fea8b..0000000000 --- a/iosApp/flare/Assets.xcassets/fa-xmark.symbolset/fa-xmark.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - Weight/Scale Variations - Ultralight - Thin - Light - Regular - Medium - Semibold - Bold - Heavy - Black - - - - - - - - - - - Design Variations - Symbols are supported in up to nine weights and three scales. - For optimal layout with text and other symbols, vertically align - symbols with the adjacent text. - - - - - - Margins - Leading and trailing margins on the left and right side of each symbol - can be adjusted by modifying the x-location of the margin guidelines. - Modifications are automatically applied proportionally to all - scales and weights. - - - - Exporting - Symbols should be outlined when exporting to ensure the - design is preserved when submitting to Xcode. - Template v.6.0 - Requires Xcode 16 or greater - Generated from square - Typeset at 100.0 points - Small - Medium - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iosApp/flare/Common/FoundationModelOnDeviceAI.swift b/iosApp/flare/Common/FoundationModelOnDeviceAI.swift deleted file mode 100644 index 1f43d15b48..0000000000 --- a/iosApp/flare/Common/FoundationModelOnDeviceAI.swift +++ /dev/null @@ -1,42 +0,0 @@ -import Foundation -import KotlinSharedUI -import FoundationModels - -final class FoundationModelOnDeviceAI: SwiftOnDeviceAI { - private init() {} - - static let shared = FoundationModelOnDeviceAI() - - func __isAvailable() async throws -> KotlinBoolean { - if #available(iOS 26.0, *) { - return KotlinBoolean(bool: SystemLanguageModel.default.isAvailable) - } - return KotlinBoolean(bool: false) - } - - func __translate(source: String, targetLanguage: String, prompt: String) async throws -> String? { - return await generateText(prompt: prompt) - } - - func __tldr(source: String, targetLanguage: String, prompt: String) async throws -> String? { - return await generateText(prompt: prompt) - } - - private func generateText(prompt: String) async -> String? { - guard ((try? await __isAvailable())?.boolValue ?? false) else { - return nil - } - - if #available(iOS 26.0, *) { - do { - let session = LanguageModelSession() - let response = try await session.respond(to: prompt) - return response.content - } catch { - return nil - } - } - - return nil - } -} diff --git a/iosApp/flare/UI/AgentTrace+LocalizedLabel.swift b/iosApp/flare/UI/AgentTrace+LocalizedLabel.swift deleted file mode 100644 index 65cd89c7f6..0000000000 --- a/iosApp/flare/UI/AgentTrace+LocalizedLabel.swift +++ /dev/null @@ -1,88 +0,0 @@ -import Foundation -import KotlinSharedUI - -extension AgentTrace { - var localizedLabel: String { - if let toolKey { - return toolKey.localizedLabel - } - - switch phase { - case .loadingPostContext: - return String(localized: "status_insight_trace_loading_post_context") - case .postContextLoaded: - return String(localized: "status_insight_trace_post_context_loaded") - case .preparingImages: - return String(localized: "status_insight_trace_preparing_images") - case .imagesUnsupportedFallback: - return String(localized: "status_insight_trace_images_unsupported_fallback") - case .agentStarted: - return String(localized: "status_insight_trace_agent_started") - case .strategyStarted: - return String(localized: "status_insight_trace_strategy_started") - case .strategyCompleted: - return String(localized: "status_insight_trace_strategy_completed") - case .subgraphStarted: - return String(localized: "status_insight_trace_subgraph_started") - case .subgraphCompleted: - return String(localized: "status_insight_trace_subgraph_completed") - case .subgraphFailed: - return String(localized: "status_insight_trace_subgraph_failed") - case .askingModel: - return String(format: String(localized: "status_insight_trace_asking_model"), detail ?? "") - case .modelResponseReceived: - return String(localized: "status_insight_trace_model_response_received") - case .streamingStarted: - return String(format: String(localized: "status_insight_trace_streaming_started"), detail ?? "") - case .streamingResponse: - return String(localized: "status_insight_trace_streaming_response") - case .streamingCompleted: - return String(localized: "status_insight_trace_streaming_completed") - case .streamingFailed: - return String(localized: "status_insight_trace_streaming_failed") - case .runningStep: - return String(localized: "status_insight_trace_running_step") - case .stepCompleted: - return String(localized: "status_insight_trace_step_completed") - case .stepFailed: - return String(localized: "status_insight_trace_step_failed") - case .toolCallStarted: - return detail ?? String(localized: "status_insight_trace_running_step") - case .toolCallCompleted: - return detail ?? String(localized: "status_insight_trace_step_completed") - case .toolValidationFailed: - return detail ?? String(localized: "status_insight_trace_tool_validation_failed") - case .toolCallFailed: - return detail ?? String(localized: "status_insight_trace_tool_call_failed") - case .agentCompleted: - return String(localized: "status_insight_trace_agent_completed") - case .agentFailed: - return String(localized: "status_insight_trace_agent_failed") - case .agentClosing: - return String(localized: "status_insight_trace_agent_closing") - } - } -} - -private extension AgentToolKey { - var localizedLabel: String { - switch self { - case .loadStatusContextStarted: - return String(localized: "status_insight_trace_tool_load_status_context_started") - case .loadStatusContextCompleted: - return String(localized: "status_insight_trace_tool_load_status_context_completed") - case .loadStatusContextValidationFailed: - return String(localized: "status_insight_trace_tool_load_status_context_validation_failed") - case .loadStatusContextFailed: - return String(localized: "status_insight_trace_tool_load_status_context_failed") - case .searchPostsStarted, .searchUsersStarted: - return String(localized: "status_insight_trace_tool_search_status_started") - case .searchPostsCompleted, .searchUsersCompleted: - return String(localized: "status_insight_trace_tool_search_status_completed") - case .searchPostsValidationFailed, .searchUsersValidationFailed: - return String(localized: "status_insight_trace_tool_search_status_validation_failed") - case .searchPostsFailed, .searchUsersFailed: - return String(localized: "status_insight_trace_tool_search_status_failed") - } - } -} diff --git a/iosApp/flare/UI/Component/AdaptiveTimelineCard.swift b/iosApp/flare/UI/Component/AdaptiveTimelineCard.swift deleted file mode 100644 index 6ec195043b..0000000000 --- a/iosApp/flare/UI/Component/AdaptiveTimelineCard.swift +++ /dev/null @@ -1,27 +0,0 @@ -import SwiftUI -import KotlinSharedUI - -struct AdaptiveTimelineCard: View { - @Environment(\.timelineAppearance.timelineDisplayMode) private var timelineDisplayMode - @Environment(\.isMultipleColumn) private var isMultipleColumn - let index: Int - let totalCount: Int - @ViewBuilder - let content: () -> Content - - var body: some View { - if isMultipleColumn || !(timelineDisplayMode == .plain) { - ListCardView(index: index, totalCount: totalCount) { - content() - } - .padding(.horizontal) - } else { - VStack(spacing: 0) { - content() - if totalCount <= 0 || index < totalCount - 1 { - Divider() - } - } - } - } -} diff --git a/iosApp/flare/UI/Component/KotlinPresenter.swift b/iosApp/flare/UI/Component/KotlinPresenter.swift deleted file mode 100644 index c0916c9d40..0000000000 --- a/iosApp/flare/UI/Component/KotlinPresenter.swift +++ /dev/null @@ -1,33 +0,0 @@ -@preconcurrency import KotlinSharedUI -import Foundation -@preconcurrency import Combine - -// using @Observable might init presenter multiple times -// which is a heavy work since Kotlin Presenter is not designed to do so -// so we keep using the old ObservableObject and @StateObject -// see: https://github.com/Dimillian/IceCubesApp/issues/2033 -final class KotlinPresenter: ObservableObject { - private var subscribers = Set() - var presenter: PresenterBase - let key = UUID().uuidString - - init(presenter: PresenterBase) { - self.presenter = presenter - self.state = presenter.models.value - self.presenter.models.toPublisher().receive(on: DispatchQueue.main).sink { [weak self] newState in - guard let self, self.state !== newState else { return } - self.state = newState - }.store(in: &subscribers) - } - - @Published - var state: T - -// @MainActor - deinit { - subscribers.forEach { cancellable in - cancellable.cancel() - } - presenter.close() - } -} diff --git a/iosApp/flare/UI/Component/MediaView.swift b/iosApp/flare/UI/Component/MediaView.swift deleted file mode 100644 index e79f7a3a9b..0000000000 --- a/iosApp/flare/UI/Component/MediaView.swift +++ /dev/null @@ -1,148 +0,0 @@ -import SwiftUI -import TipKit -import KotlinSharedUI -import VideoPlayer -import AVFoundation - -struct MediaView: View { - let data: UiMedia - - init(data: UiMedia) { - self.data = data - } - - var body: some View { - ZStack { - switch onEnum(of: data) { - case .image(let image): - Color.gray -// .opacity(0.2) - .overlay { - NetworkImage(data: image.previewUrl, customHeader: image.customHeaders) - .allowsHitTesting(false) - } - .clipped() - case .video(let video): - MediaVideoView(data: video) - case .gif(let gif): - Color.gray -// .opacity(0.2) - .overlay { - NetworkImage(data: gif.url, customHeader: gif.customHeaders) - .allowsHitTesting(false) - } - .clipped() - case .audio(let audio): - EmptyView() - } - } - } -} - -struct MediaVideoView: View { - @Environment(\.timelineAppearance.videoAutoplay) private var videoAutoplay - @Environment(\.networkKind) private var networkKind - @Environment(\.isScrolling) private var isScrolling - @Environment(\.isScrollingState) private var isScrollingState - @State private var play: Bool = false - @State private var videoState: VideoState = .idle - @State private var time: CMTime = .zero - @State private var isAppeared: Bool = false - let data: UiMediaVideo - - private var effectiveIsScrolling: Bool { - isScrollingState?.isScrolling ?? isScrolling - } - - func canPlay() -> Bool { - switch videoAutoplay { - case .always: - return true - case .wifi: - return networkKind == .wifi - case .never: - return false - } - } - - var body: some View { - Color.gray -// .opacity(0.2) - .overlay { - NetworkImage(data: data.thumbnailUrl, customHeader: data.customHeaders) - .allowsHitTesting(false) - } - .clipped() - .overlay { - if let videoURL = URL(string: data.url) { - VideoPlayer(url: videoURL, play: $play, time: $time) - .mute(true) - .autoReplay(true) - .onStateChanged { state in - switch state { - case .playing(let duration): videoState = .playing(duration) - case .loading: videoState = .loading - case .paused: videoState = .idle - case .error(let error): videoState = .error(error) - } - } - .contentMode(.scaleAspectFill) - .onChange(of: effectiveIsScrolling, { oldValue, newValue in - if !newValue, !play, isAppeared, canPlay() { - play = true - } - }) - .onAppear { - isAppeared = true - if !effectiveIsScrolling, canPlay() { - play = true - } - } - .onDisappear { - isAppeared = false - play = false - } - .allowsHitTesting(false) - } - } - .overlay(alignment: .bottomLeading) { - switch videoState { - case .idle: - Image("fa-circle-play") - .foregroundStyle(Color(.white)) - .padding(8) - .background(.black, in: .rect(cornerRadius: 16)) - .padding() - case .loading: - ProgressView() - .tint(.white) - .padding(8) - .background(.black, in: .rect(cornerRadius: 16)) - .padding() - case .playing(let duration): - Text( - Date(timeIntervalSinceNow: duration - time.seconds), - style: .timer - ) - .font(.caption) - .foregroundStyle(Color(.white)) - .padding(8) - .background(.black, in: .rect(cornerRadius: 16)) - .padding() - case .error: - Image(systemName: "exclamationmark.triangle.fill") - .foregroundStyle(Color(.white)) - .padding(8) - .background(.black, in: .rect(cornerRadius: 16)) - .padding() - } - } - } - - enum VideoState { - case idle - case loading - case playing(Double) - case error(Error) - } -} diff --git a/iosApp/flare/UI/Component/PagingView.swift b/iosApp/flare/UI/Component/PagingView.swift deleted file mode 100644 index 49b4cff8bd..0000000000 --- a/iosApp/flare/UI/Component/PagingView.swift +++ /dev/null @@ -1,150 +0,0 @@ -import SwiftUI -import KotlinSharedUI - -struct PagingView: View { - let data: PagingState - @ViewBuilder - let emptyContent: () -> EmptyContent - @ViewBuilder - let errorContent: (KotlinThrowable, @escaping () -> Void) -> ErrorContent - @ViewBuilder - let loadingContent: (Int, Int) -> LoadingContent - let loadingCount = 5 - @ViewBuilder - let successContent: (T, Int, Int) -> SuccessContent - var body: some View { - switch onEnum(of: data) { - case .empty: emptyContent() - case .error(let error): errorContent(error.error) { - _ = error.onRetry() - } - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) - case .loading: ForEach(0.., - @ViewBuilder - successContent: @escaping (T) -> SuccessContent, - @ViewBuilder - loadingContent: @escaping () -> LoadingContent - ) where ErrorContent == ListErrorView, EmptyContent == ListEmptyView { - self.init( - data: data, - emptyContent: { ListEmptyView() }, - errorContent: { error, retry in - ListErrorView(error: error) { - retry() - } - }, - loadingContent: { index, loadingCount in - loadingContent() - }, - successContent: { item, index, itemCount in - successContent(item) - } - ) - } - - - init( - data: PagingState, - @ViewBuilder - successContent: @escaping (T) -> SuccessContent, - @ViewBuilder - loadingContent: @escaping () -> LoadingContent, - @ViewBuilder - errorContent: @escaping (KotlinThrowable, @escaping () -> Void) -> ErrorContent - ) where EmptyContent == ListEmptyView { - self.init( - data: data, - emptyContent: { ListEmptyView() }, - errorContent: { error, retry in - errorContent(error, retry) - }, - loadingContent: { index, loadingCount in - loadingContent() - }, - successContent: { item, index, itemCount in - successContent(item) - } - ) - } - - - init( - data: PagingState, - @ViewBuilder - successContent: @escaping (T) -> SuccessContent, - @ViewBuilder - loadingContent: @escaping () -> LoadingContent, - @ViewBuilder - errorContent: @escaping (KotlinThrowable, @escaping () -> Void) -> ErrorContent, - @ViewBuilder - emptyContent: @escaping () -> EmptyContent - ) { - self.init( - data: data, - emptyContent: { emptyContent() }, - errorContent: { error, retry in - errorContent(error, retry) - }, - loadingContent: { index, loadingCount in - loadingContent() - }, - successContent: { item, index, itemCount in - successContent(item) - } - ) - } - -} - - -struct UserPagingView: View { - @Environment(\.openURL) private var openURL - let data: PagingState - var body: some View { - PagingView(data: data) { user in - UserCompatView(data: user) - .onTapGesture { - user.onClicked(ClickContext(launcher: AppleUriLauncher(openUrl: openURL))) - } - } loadingContent: { - UserLoadingView() - .padding(.vertical, 8) - } - } -} diff --git a/iosApp/flare/UI/Component/Status/StatusActionView.swift b/iosApp/flare/UI/Component/Status/StatusActionView.swift deleted file mode 100644 index 263a9b46f7..0000000000 --- a/iosApp/flare/UI/Component/Status/StatusActionView.swift +++ /dev/null @@ -1,353 +0,0 @@ -import SwiftUI -import KotlinSharedUI -import SwiftUIBackports -import UIKit - -// MARK: - Top-level container -// Hoists @ScaledMetric, @Environment reads to a single place -// instead of duplicating them in every child view instance. - -struct StatusActionsView: View { - @Environment(\.timelineAppearance.postActionStyle) private var postActionStyle - @Environment(\.timelineAppearance.showNumbers) private var showNumbers - @Environment(\.timelineAppearance.postActionLayout) private var postActionLayout - @Environment(\.openURL) private var openURL - @ScaledMetric(relativeTo: .footnote) var fontSize = 13 - let data: [ActionMenu] - let useText: Bool - var allowSpacer: Bool = true - var applyPostActionLayout: Bool = true - - var body: some View { - let actions = resolvedData - if useText { - ForEach(0.. some View { - if let color { - self.foregroundStyle(color) - } else { - self - } - } -} - -extension ActionMenu.ItemColor { - var swiftColor: Color? { - switch self { - case .red: return .red - case .contentColor: return .primary - case .primaryColor: return .accentColor - default: return nil - } - } - - var role: ButtonRole? { - switch self { - case .red: - .destructive - case .primaryColor: - if #available(iOS 26.0, *) { - .confirm - } else { - nil - } - default: - nil - } - } -} - -extension ActionMenuItemText { - var resolvedString: LocalizedStringResource { - switch onEnum(of: self) { - case .raw(let raw): - return LocalizedStringResource(stringLiteral: raw.text) - case .localized(let localized): - switch localized.type { - case .like: return "like" - case .unlike: return "unlike" - case .retweet: return "retweet" - case .unretweet: return "retweet_remove" - case .reply: return "reply" - case .comment: return "comment" - case .quote: return "quote" - case .bookmark: return "bookmark_add" - case .unbookmark: return "bookmark_remove" - case .more: return "more" - case .delete: return "delete" - case .report: return "report" - case .react: return "reaction_add" - case .share: return "share" - case .fxShare: return "fx_share" - case .unReact: return "reaction_remove" - case .editUserList: return "edit_user_in_list" - case .sendMessage: return "send_message" - case .mute: return "mute" - case .unMute: return "unmute" - case .block: return "block" - case .unBlock: return "unblock" - case .blockWithHandleParameter: return "block_user_with_handle \(localized.parameters.first ?? "")" - case .muteWithHandleParameter: return "mute_user_with_handle \(localized.parameters.first ?? "")" - case .acceptFollowRequest: return "accept_follow_request" - case .rejectFollowRequest: return "reject_follow_request" - case .retryTranslation: return "Retry translation" - case .translate: return "Translate" - case .showOriginal: return "Show original" - case .favorite: return "Favourite" - case .unFavorite: return "Unfavourite" - } - } - } -} - -struct StatusActionIcon: View { - let icon: UiIcon? - - var body: some View { - if let icon = icon { - icon.image - } - } -} - -private func castActionMenus(_ value: Any) -> [ActionMenu] { - if let actions = value as? [ActionMenu] { - return actions - } - if let actions = value as? NSArray { - return actions.cast(ActionMenu.self) - } - return [] -} - -extension UiIcon { - var image: Image { - Image(imageName) - } - - var imageName: String { - switch self { - case .home: return "fa-house" - case .notification: return "fa-bell" - case .search: return "fa-magnifying-glass" - case .profile: return "fa-circle-user" - case .settings: return "fa-gear" - case .local: return "fa-users" - case .world: return "fa-globe" - case .featured: return "fa-rectangle-list" - case .bookmark: return "fa-bookmark" - case .unbookmark: return "fa-bookmark.fill" - case .delete: return "fa-trash" - case .like: return "fa-heart" - case .unlike: return "fa-heart.fill" - case .more: return "fa-ellipsis" - case .quote: return "fa-reply" - case .react: return "fa-plus" - case .unReact: return "fa-minus" - case .reply: return "fa-reply" - case .report: return "fa-circle-info" - case .retweet: return "fa-retweet" - case .unretweet: return "fa-retweet" - case .comment: return "fa-comment-dots" - case .share: return "fa-share-nodes" - case .moreVerticel: return "fa-ellipsis-vertical" - case .list: return "fa-list" - case .chatMessage: return "fa-message" - case .mute: return "fa-volume-xmark" - case .unMute: return "fa-volume-xmark" - case .block: return "fa-user-slash" - case .unBlock: return "fa-user-slash" - case .follow: return "fa-user-plus" - case .favourite: return "fa-star-fill" - case .unFavourite: return "fa-star" - case .mention: return "fa-at" - case .poll: return "fa-square-poll-horizontal" - case .edit: return "fa-pen" - case .info: return "fa-circle-info" - case .pin: return "fa-thumbtack" - case .check: return "fa-check" - case .feeds: return "fa-square-rss" - case .messages: return "fa-message" - case .rss: return "fa-square-rss" - case .channel: return "fa-tv" - case .heart: return "fa-heart" - case .mastodon: return "fa-mastodon" - case .misskey: return "fa-misskey" - case .bluesky: return "fa-bluesky" - case .nostr: return "fa-nostr" - case .twitter: return "fa-twitter" - case .x: return "fa-x-twitter" - case .weibo: return "fa-weibo" - case .translate: return "fa-language" - case .pixiv: return "fa-pixiv" - } - } -} diff --git a/iosApp/flare/UI/Component/Status/StatusVisibilityView.swift b/iosApp/flare/UI/Component/Status/StatusVisibilityView.swift deleted file mode 100644 index 969451d245..0000000000 --- a/iosApp/flare/UI/Component/Status/StatusVisibilityView.swift +++ /dev/null @@ -1,15 +0,0 @@ -import SwiftUI -import KotlinSharedUI - -struct StatusVisibilityView: View { - let data: UiTimelineV2.PostVisibility - var body: some View { - switch data { - case .public: Image("fa-globe") - case .home: Image("fa-lock-open") - case .followers: Image("fa-lock") - case .specified: Image("fa-at") - case .channel: Image(.faTv) - } - } -} diff --git a/iosApp/flare/UI/Component/TabIcon.swift b/iosApp/flare/UI/Component/TabIcon.swift deleted file mode 100644 index 58dd4357e8..0000000000 --- a/iosApp/flare/UI/Component/TabIcon.swift +++ /dev/null @@ -1,192 +0,0 @@ -import SwiftUI -import KotlinSharedUI - -struct TabTitle: View { - let title: Any - var body: some View { - Text(String(describing: title)) - } -} - -struct TimelineTabTitle: View { - let title: UiText - var body: some View { - Text(title.text) - } -} - -extension UiText { - var text: String { - switch onEnum(of: self) { - case .localized(let localized): - return localized.string.text - case .raw(let raw): - return raw.string - } - } -} - -extension UiStrings { - var text: String { - switch self { - case .home: String(localized: "home_tab_home_title") - case .notifications: String(localized: "home_tab_notifications_title") - case .discover: String(localized: "home_tab_discover_title") - case .me: String(localized: "home_tab_me_title") - case .settings: String(localized: "settings_title") - case .mastodonLocal: String(localized: "mastodon_tab_local_title") - case .mastodonPublic: String(localized: "mastodon_tab_public_title") - case .featured: String(localized: "home_tab_featured_title") - case .bookmark: String(localized: "home_tab_bookmarks_title") - case .favourite: String(localized: "home_tab_favorite_title") - case .list: String(localized: "home_tab_list_title") - case .feeds: String(localized: "home_tab_feeds_title") - case .directMessage: String(localized: "dm_list_title") - case .rss: String(localized: "rss_title") - case .social: String(localized: "social_title") - case .antenna: String(localized: "antenna_title") - case .mixedTimeline: String(localized: "mixed_timeline_title") - case .liked: String(localized: "liked_tab_title") - case .allRssFeeds: String(localized: "all_rss_feeds_title") - case .posts: String(localized: "posts_title") - case .postsWithReplies: String(localized: "posts_with_replies_title", defaultValue: "Posts & Replies") - case .media: String(localized: "media_title", defaultValue: "Media") - case .channel: String(localized: "channel_title") - case .default: String(localized: "tab_settings_default") - case .login: String(localized: "login_button") - case .verify: String(localized: "verify_button", defaultValue: "Verify") - case .cancel: String(localized: "cancel_button") - case .next: String(localized: "service_select_next_button") - case .username: String(localized: "bluesky_login_username_hint") - case .password: String(localized: "bluesky_login_password_hint") - case .otp: String(localized: "bluesky_login_auth_factor_token_hint") - case .oauthLogin: String(localized: "bluesky_login_oauth_button") - case .passwordLogin: String(localized: "bluesky_login_use_password_button") - case .qrConnect: String(localized: "nostr_login_qr_button") - case .credentialImport: String(localized: "nostr_login_title") - case .externalSigner: String(localized: "nostr_login_amber_button") - case .webCookieLogin: String(localized: "login_button") - case .nostrLoginAccount: String(localized: "nostr_login_account_hint") - case .following: String(localized: "misskey_channel_tab_following") - case .pixivRankingWeek: String(localized: "pixiv_ranking_week_title", defaultValue: "Weekly Ranking") - case .pixivRankingMonth: String(localized: "pixiv_ranking_month_title", defaultValue: "Monthly Ranking") - case .pixivRankingDayMale: String(localized: "pixiv_ranking_day_male_title", defaultValue: "Male Ranking") - case .pixivRankingDayFemale: String(localized: "pixiv_ranking_day_female_title", defaultValue: "Female Ranking") - case .pixivRankingWeekOriginal: String(localized: "pixiv_ranking_week_original_title", defaultValue: "Original Ranking") - case .pixivRankingWeekRookie: String(localized: "pixiv_ranking_week_rookie_title", defaultValue: "Rookie Ranking") - case .pixivRankingDayManga: String(localized: "pixiv_ranking_day_manga_title", defaultValue: "Manga Ranking") - case .illustrations: String(localized: "illustrations_title", defaultValue: "Illustrations") - case .manga: String(localized: "manga_title", defaultValue: "Manga") - } - } -} - -struct TabIcon: View { - let icon: IconType - let size: CGFloat - let iconOnly: Bool - - init( - icon: IconType, - size: CGFloat = 20, - iconOnly: Bool = false - ) { - self.icon = icon - self.size = size - self.iconOnly = iconOnly - } - - var body: some View { - switch onEnum(of: icon) { - case .material(let material): - MaterialTabIcon(icon: material.icon) - .frame(width: size, height: size) - case .avatar(let avatar): - AvatarTabIcon(userKey: avatar.accountKey, accountType: AccountType.Specific(accountKey: avatar.accountKey)) - .frame(width: size, height: size) - case .url(let url): - NetworkImage(data: url.url) - .frame(width: size, height: size) - case .mixed(let mixed): - if iconOnly { - MaterialTabIcon(icon: mixed.icon) - .frame(width: size, height: size) - } else { - ZStack( - alignment: .bottomTrailing - ) { - AvatarTabIcon(userKey: mixed.accountKey, accountType: AccountType.Specific(accountKey: mixed.accountKey)) - .frame(width: size, height: size) - MaterialTabIcon(icon: mixed.icon) - .padding(2) - .background(Color.white) - .foregroundStyle(Color.black) - .clipShape(.circle) - .frame(width: size / 2, height: size / 2) - } - .frame(width: size, height: size) - } - case .favIcon(let favIcon): - FavTabIcon(host: favIcon.host) - .frame(width: size, height: size) - } - } -} - -extension TabIcon { - init( - tabItem: UiTimelineTabItem, - size: CGFloat = 20, - iconOnly: Bool = false - ) { - self.init(icon: tabItem.icon, size: size, iconOnly: iconOnly) - } -} - -struct MaterialTabIcon: View { - let icon: UiIcon - var body: some View { - Image(icon.imageName) - .resizable() - .scaledToFit() - } -} - -struct AvatarTabIcon: View { - - @StateObject private var presenter: KotlinPresenter - - init(userKey: MicroBlogKey, accountType: AccountType) { - self._presenter = .init(wrappedValue: .init(presenter: UserPresenter(accountType: accountType, userKey: userKey))) - } - - var body: some View { - StateView(state: presenter.state.user) { user in - AvatarView(data: user.avatar?.url, customHeader: user.avatar?.customHeaders) - } loadingContent: { - Image("fa-globe") - .resizable() - .scaledToFit() - .redacted(reason: .placeholder) - } - } -} - -struct FavTabIcon: View { - @StateObject private var presenter: KotlinPresenter> - - init(host: String) { - self._presenter = .init(wrappedValue: .init(presenter: FavIconPresenter(host: host))) - } - - var body: some View { - StateView(state: presenter.state) { url in - NetworkImage(data: .init(url)) - } loadingContent: { - Image("fa-globe") - .resizable() - .scaledToFit() - .redacted(reason: .placeholder) - } - } -} diff --git a/iosApp/flare/UI/Component/TranslateStatusComponent.swift b/iosApp/flare/UI/Component/TranslateStatusComponent.swift deleted file mode 100644 index b8896f9cd4..0000000000 --- a/iosApp/flare/UI/Component/TranslateStatusComponent.swift +++ /dev/null @@ -1,17 +0,0 @@ -import KotlinSharedUI -import SwiftUI - -struct TranslateStatusComponent: View { - let data: TranslationDisplayState - - var body: some View { - HStack { - Image(.faLanguage) - switch data { - case .failed: Image(.faCircleExclamation) - case .translating: ProgressView().progressViewStyle(.circular).scaledToFit().frame(width: 12, height: 12) - default: EmptyView() - } - } - } -} diff --git a/iosApp/scripts/generate_icon_previews.sh b/iosApp/scripts/generate_icon_previews.sh deleted file mode 100755 index 633cabae30..0000000000 --- a/iosApp/scripts/generate_icon_previews.sh +++ /dev/null @@ -1,403 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -icon_dir="iosApp/flare" -asset_catalog="iosApp/flare/Assets.xcassets" -swift_output="iosApp/flare/Common/AppIconOption.swift" -project_file="iosApp/project.yml" -prefix="app_icon_preview" -size="256" -platform="iOS" -rendition="Default" -overwrite="false" -generate_previews="true" -generate_appiconsets="true" -generate_swift="true" -update_project="true" -ictool="/Applications/Xcode.app/Contents/Applications/Icon Composer.app/Contents/Executables/ictool" - -usage() { - cat <<'EOF' -Generate regular image assets from Icon Composer .icon bundles. - -Usage: - iosApp/scripts/generate_icon_previews.sh [options] - -Options: - --icon-dir Directory containing AppIcon*.icon bundles. Default: iosApp/flare - --asset-catalog Output .xcassets directory. Default: iosApp/flare/Assets.xcassets - --swift-output Output AppIconOption.swift path. Default: iosApp/flare/Common/AppIconOption.swift - --project-file XcodeGen project spec to update. Default: iosApp/project.yml - --prefix Output image asset prefix. Default: app_icon_preview - --size PNG width/height. Default: 256 - --platform ictool platform. Default: iOS - --rendition ictool rendition. Default: Default - --ictool Path to Icon Composer ictool. - --overwrite Replace existing preview .imageset directories. - --skip-appiconsets Do not generate alternate .appiconset assets. - --skip-previews Only generate the Swift icon list. - --skip-swift Only generate preview image assets. - --skip-project Do not update alternate app icon names in the XcodeGen spec. - -h, --help Show this help. -EOF -} - -while [[ $# -gt 0 ]]; do - case "$1" in - --icon-dir) - icon_dir="$2" - shift 2 - ;; - --asset-catalog) - asset_catalog="$2" - shift 2 - ;; - --swift-output) - swift_output="$2" - shift 2 - ;; - --project-file) - project_file="$2" - shift 2 - ;; - --prefix) - prefix="$2" - shift 2 - ;; - --size) - size="$2" - shift 2 - ;; - --platform) - platform="$2" - shift 2 - ;; - --rendition) - rendition="$2" - shift 2 - ;; - --ictool) - ictool="$2" - shift 2 - ;; - --overwrite) - overwrite="true" - shift - ;; - --skip-appiconsets) - generate_appiconsets="false" - shift - ;; - --skip-previews) - generate_previews="false" - shift - ;; - --skip-swift) - generate_swift="false" - shift - ;; - --skip-project) - update_project="false" - shift - ;; - -h|--help) - usage - exit 0 - ;; - *) - echo "error: unknown argument: $1" >&2 - usage >&2 - exit 1 - ;; - esac -done - -if [[ ( "$generate_previews" == "true" || "$generate_appiconsets" == "true" ) && ! -x "$ictool" ]]; then - echo "error: ictool not found at: $ictool" >&2 - echo "Install/open Xcode with Icon Composer, or pass --ictool ." >&2 - exit 1 -fi - -if [[ ! -d "$icon_dir" ]]; then - echo "error: icon directory does not exist: $icon_dir" >&2 - exit 1 -fi - -if [[ ( "$generate_previews" == "true" || "$generate_appiconsets" == "true" ) && ! -d "$asset_catalog" ]]; then - echo "error: asset catalog does not exist: $asset_catalog" >&2 - exit 1 -fi - -if [[ "$generate_previews" == "true" || "$generate_appiconsets" == "true" ]] && { ! [[ "$size" =~ ^[0-9]+$ ]] || [[ "$size" -le 0 ]]; }; then - echo "error: --size must be a positive integer" >&2 - exit 1 -fi - -export_size="$size" -if command -v sips >/dev/null 2>&1 && [[ "$size" -gt 1 ]]; then - export_size="$((size + 1))" -fi - -shopt -s nullglob -icon_bundles=("$icon_dir"/AppIcon*.icon) -shopt -u nullglob - -if [[ "${#icon_bundles[@]}" -eq 0 ]]; then - echo "error: no AppIcon*.icon bundles found in: $icon_dir" >&2 - exit 1 -fi - -IFS=$'\n' icon_bundles=($(printf '%s\n' "${icon_bundles[@]}" | sort | awk ' - /\/AppIcon\.icon$/ { - primary = $0 - next - } - { - print - } - END { - if (primary != "") { - print primary - } - } -')) -unset IFS - -preview_suffix_for_icon_name() { - local icon_name="$1" - - if [[ "$icon_name" == "AppIcon" ]]; then - echo "default" - elif [[ "$icon_name" == AppIcon_* ]]; then - echo "${icon_name#AppIcon_}" - else - echo "$icon_name" - fi -} - -title_for_icon_name() { - local icon_name="$1" - local suffix - - suffix="$(preview_suffix_for_icon_name "$icon_name")" - if [[ "$suffix" == "default" ]]; then - echo "Default" - else - printf '%s\n' "$suffix" | tr '_' ' ' | awk '{ - for (i = 1; i <= NF; i++) { - $i = toupper(substr($i, 1, 1)) substr($i, 2) - } - print - }' - fi -} - -normalize_png() { - local png_path="$1" - - if command -v sips >/dev/null 2>&1; then - sips -Z "$size" "$png_path" --out "$png_path" >/dev/null - fi -} - -if [[ "$generate_swift" == "true" ]]; then - mkdir -p "$(dirname "$swift_output")" - - { - cat <<'EOF' -import Foundation - -struct AppIconOption: Identifiable { - let title: String - let alternateIconName: String? - let previewImageName: String - - var id: String { - alternateIconName ?? "AppIcon" - } -} - -extension AppIconOption { - static let all: [AppIconOption] = [ -EOF - - for icon_bundle in "${icon_bundles[@]}"; do - icon_name="$(basename "$icon_bundle" .icon)" - suffix="$(preview_suffix_for_icon_name "$icon_name")" - title="$(title_for_icon_name "$icon_name")" - asset_name="${prefix}_${suffix}" - - if [[ "$icon_name" == "AppIcon" ]]; then - echo " .init(title: \"$title\", alternateIconName: nil, previewImageName: \"$asset_name\")," - else - echo " .init(title: \"$title\", alternateIconName: \"$icon_name\", previewImageName: \"$asset_name\")," - fi - done - - cat <<'EOF' - ] - - static func previewImageName(for alternateIconName: String?) -> String { - all.first { $0.alternateIconName == alternateIconName }?.previewImageName ?? "app_icon_preview_default" - } -} -EOF - } > "$swift_output" - - echo "Generated $swift_output" -fi - -if [[ "$update_project" == "true" ]]; then - if [[ ! -f "$project_file" ]]; then - echo "error: Xcode project spec does not exist: $project_file" >&2 - exit 1 - fi - - alternate_names=() - for icon_bundle in "${icon_bundles[@]}"; do - icon_name="$(basename "$icon_bundle" .icon)" - if [[ "$icon_name" != "AppIcon" ]]; then - alternate_names+=("$icon_name") - fi - done - - alternate_names_string="${alternate_names[*]}" - if grep -q "ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES:" "$project_file"; then - perl -0pi -e "s/ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES: \"[^\"]*\"/ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES: \"$alternate_names_string\"/g" "$project_file" - echo "Updated alternate app icon names in $project_file" - elif grep -q "ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES" "$project_file"; then - perl -0pi -e "s/ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = \"[^\"]*\";/ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = \"$alternate_names_string\";/g" "$project_file" - echo "Updated alternate app icon names in $project_file" - else - echo "warning: ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES was not found in $project_file" >&2 - fi - - perl -0pi -e 's/ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS: YES/ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS: NO/g' "$project_file" - perl -0pi -e 's/ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;/ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;/g' "$project_file" - echo "Disabled include-all app icon assets in $project_file" - - if grep -q "membershipExceptions = (" "$project_file"; then - exception_lines=$'\t\t\t\tInfo.plist,\n' - for icon_name in "${alternate_names[@]}"; do - exception_lines+=$'\t\t\t\t'"$icon_name.icon"$',\n' - done - - EXCEPTION_LINES="$exception_lines" perl -0pi -e ' - my $lines = $ENV{"EXCEPTION_LINES"}; - s/membershipExceptions = \(\n.*?\t\t\t\);/membershipExceptions = (\n$lines\t\t\t);/s; - ' "$project_file" - echo "Excluded alternate .icon sources from the Flare target in $project_file" - fi -fi - -if [[ "$generate_previews" != "true" && "$generate_appiconsets" != "true" ]]; then - exit 0 -fi - -for icon_bundle in "${icon_bundles[@]}"; do - icon_name="$(basename "$icon_bundle" .icon)" - suffix="$(preview_suffix_for_icon_name "$icon_name")" - - if [[ "$generate_previews" == "true" ]]; then - asset_name="${prefix}_${suffix}" - imageset="$asset_catalog/${asset_name}.imageset" - png_name="${asset_name}.png" - png_path="$imageset/$png_name" - - if [[ -e "$imageset" && "$overwrite" != "true" ]]; then - echo "Skipping existing $(basename "$imageset"). Use --overwrite to replace it." - else - if [[ -e "$imageset" ]]; then - rm -rf "$imageset" - fi - - mkdir -p "$imageset" - - if ! "$ictool" "$icon_bundle" \ - --export-image \ - --output-file "$png_path" \ - --platform "$platform" \ - --rendition "$rendition" \ - --width "$export_size" \ - --height "$export_size" \ - --scale 1; then - rm -rf "$imageset" - echo "error: ictool failed for $icon_bundle" >&2 - echo "If this happens only inside Codex, run this script from Terminal or allow the ictool command outside the sandbox." >&2 - exit 1 - fi - - normalize_png "$png_path" - - cat > "$imageset/Contents.json" <&2 - echo "If this happens only inside Codex, run this script from Terminal or allow the ictool command outside the sandbox." >&2 - exit 1 - fi - - normalize_png "$png_path" - - cat > "$appiconset/Contents.json" <