[
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: [Finb]\n"
  },
  {
    "path": ".github/workflows/deploy-docs.yaml",
    "content": "name: Deploy Docs to Cloudflare Pages\n\non:\n  push:\n    branches:\n      - master\n    paths:\n      - 'docs/**'\n  workflow_dispatch:\n\njobs:\n  deploy:\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n      deployments: write\n    \n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Deploy to Cloudflare Pages\n        uses: cloudflare/wrangler-action@v3\n        with:\n          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}\n          accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}\n          command: pages deploy docs --branch=main --project-name=bark-docs\n"
  },
  {
    "path": ".github/workflows/testflight.yaml",
    "content": "name: Deploy to App Store \n\non:\n  workflow_dispatch:\n    inputs:\n      storeVersionNumber:\n        description: 'Store Version Number'\n        required: true\n        default: \"\"\n      buildNumber:\n        description: 'Build Number'\n        required: true\n        default: \"\"\n\njobs:\n  deploy:\n    runs-on: macos-latest\n\n    steps:      \n    - name: Checkout\n      uses: actions/checkout@v4\n\n    - name: Select Xcode Version\n      uses: maxim-lobanov/setup-xcode@v1\n      with:\n        xcode-version: 'latest-stable'\n\n    - name: Setup ruby\n      uses: ruby/setup-ruby@v1\n      with:\n        ruby-version: 2.7.2\n        bundler-cache: true\n\n    - uses: actions/cache@v4\n      id: pods-cache\n      with:\n        path: Pods\n        key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }}\n        restore-keys: |\n          ${{ runner.os }}-pods-\n\n    - name: Install Pods\n      run: pod install\n\n    - name: Build & Distribute to App Store\n      run: |\n        export LC_ALL=en_US.UTF-8\n        export LANG=en_US.UTF-8\n        bundle exec fastlane beta store_version_number:${{ github.event.inputs.storeVersionNumber }} build_number:${{ github.event.inputs.buildNumber }}\n      env:\n        APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}\n        APP_STORE_CONNECT_KEY_CONTENT: ${{ secrets.APP_STORE_CONNECT_KEY_CONTENT }}\n        BARK_KEY: ${{ secrets.BARK_KEY }}\n        MATCH_GIT_BASIC_AUTHORIZATION: ${{ secrets.MATCH_GIT_BASIC_AUTHORIZATION }}\n        MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}\n\n    - name: Archive artifacts\n      uses: actions/upload-artifact@v4\n      with:\n        name: dSYM\n        path: Bark.app.dSYM.zip\n"
  },
  {
    "path": ".github/workflows/tests.yaml",
    "content": "name: Tests\n\non:\n  push:\n    branches:\n      - master\n  pull_request:\n    branches:\n      - master\n\njobs:\n  tests:\n    runs-on: macos-latest\n\n    steps:      \n    - name: Checkout\n      uses: actions/checkout@v4\n\n    - name: Select Xcode Version\n      uses: maxim-lobanov/setup-xcode@v1\n      with:\n        xcode-version: 'latest-stable'\n\n    - name: Setup ruby\n      uses: ruby/setup-ruby@v1\n      with:\n        ruby-version: 2.7.2\n        bundler-cache: true\n\n    - uses: actions/cache@v4\n      id: pods-cache\n      with:\n        path: Pods\n        key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }}\n        restore-keys: |\n          ${{ runner.os }}-pods-\n\n    - name: Install Pods\n      run: pod install\n\n    - name: Run Tests\n      run: |\n        export LC_ALL=en_US.UTF-8\n        export LANG=en_US.UTF-8\n        bundle exec fastlane tests\n"
  },
  {
    "path": ".gitignore",
    "content": "*.mode*v*\n*.pbxuser\n*.xccheckout\n#*.xcbkptlist\n#*.xcscheme\n#*.xcworkspacedata\n*.xcuserstate\nbuild/\nPods/\n.DS_Store\n._.*\nxcuserdata\nbuildServer.json\n"
  },
  {
    "path": "Bark/AppDelegate+Realm.swift",
    "content": "//\n//  AppDelegate+Realm.swift\n//  Bark\n//\n//  Created by huangfeng on 12/18/25.\n//  Copyright © 2025 Fin. All rights reserved.\n//\n\nimport UIKit\n\nextension AppDelegate {\n    /*\n     之前数据库是放在App Groups 共享\n     但由于Realm无法解决 0xdead10cc 闪退问题\n     因此改为在主APP中存放数据库文件\n     Notification Service Extension 使用 plist 文件保存消息，供主APP读取存放到数据库中\n     */\n    func setupRealm() {\n        // 先执行数据库迁移\n        migrateRealmDatabase()\n        \n        // Tell Realm to use this new configuration object for the default Realm\n        Realm.Configuration.defaultConfiguration = kRealmDefaultConfiguration\n    }\n\n    func migrateRealmDatabase() {\n        // 检查是否已经迁移过\n        if UserDefaults.standard.bool(forKey: \"hasRealmMigrated\") {\n            return\n        }\n        \n        let fileManager = FileManager.default\n        guard let oldGroupUrl = fileManager.containerURL(forSecurityApplicationGroupIdentifier: \"group.bark\"),\n              let newDocUrl = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first\n        else {\n            return\n        }\n        \n        let oldRealmUrl = oldGroupUrl.appendingPathComponent(\"bark.realm\")\n        let newRealmUrl = newDocUrl.appendingPathComponent(\"bark.realm\")\n        \n        // 检查旧文件是否存在\n        guard fileManager.fileExists(atPath: oldRealmUrl.path) else {\n            // 旧文件不存在，标记为已迁移\n            UserDefaults.standard.set(true, forKey: \"hasRealmMigrated\")\n            return\n        }\n        \n        do {\n            // 复制主数据库文件\n            try fileManager.copyItem(at: oldRealmUrl, to: newRealmUrl)\n            \n            // 复制相关文件\n            let relatedFiles = [\"bark.realm.lock\", \"bark.realm.management\", \"bark.realm.note\"]\n            for file in relatedFiles {\n                let oldUrl = oldGroupUrl.appendingPathComponent(file)\n                let newUrl = newDocUrl.appendingPathComponent(file)\n                if fileManager.fileExists(atPath: oldUrl.path) {\n                    try? fileManager.copyItem(at: oldUrl, to: newUrl)\n                }\n            }\n            \n            // 删除旧文件\n            try fileManager.removeItem(at: oldRealmUrl)\n            for file in relatedFiles {\n                let oldUrl = oldGroupUrl.appendingPathComponent(file)\n                try? fileManager.removeItem(at: oldUrl)\n            }\n            \n            // 标记为已迁移\n            UserDefaults.standard.set(true, forKey: \"hasRealmMigrated\")\n        } catch {\n            // 迁移失败，弹出提示\n            DispatchQueue.main.async {\n                let alert = UIAlertController(\n                    title: \"Migration Failed\",\n                    message: \"Failed to migrate database file. Please contact support.\",\n                    preferredStyle: .alert\n                )\n                alert.addAction(UIAlertAction(title: \"OK\", style: .default))\n                self.window?.rootViewController?.present(alert, animated: true)\n            }\n        }\n    }\n    \n    // 处理 Notification Service Extension 保存的待处理消息, 将其存入 Realm 数据库\n    func processPendingMessages() {\n        guard let groupUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: \"group.bark\") else {\n            return\n        }\n        \n        let pendingMessagesDir = groupUrl.appendingPathComponent(\"pending_messages\")\n        \n        // 检查目录是否存在\n        guard FileManager.default.fileExists(atPath: pendingMessagesDir.path) else {\n            return\n        }\n        \n        // 获取所有 plist 文件\n        guard let fileUrls = try? FileManager.default.contentsOfDirectory(\n            at: pendingMessagesDir,\n            includingPropertiesForKeys: nil,\n            options: [.skipsHiddenFiles]\n        ) else {\n            return\n        }\n        \n        let plistFiles = fileUrls.filter { $0.pathExtension == \"plist\" }\n        \n        guard !plistFiles.isEmpty else {\n            return\n        }\n        \n        // 获取 Realm 实例\n        guard let realm = try? Realm() else {\n            return\n        }\n        \n        // 批量处理消息\n        var messagesToAdd: [Message] = []\n        var urlsToDelete: [URL] = []\n        \n        // 先读取所有 plist 文件\n        for plistUrl in plistFiles {\n            guard let dict = NSDictionary(contentsOf: plistUrl) as? [String: Any] else {\n                continue\n            }\n            \n            let message = Message(dict: dict)\n            messagesToAdd.append(message)\n            urlsToDelete.append(plistUrl)\n        }\n        \n        // 批量写入 Realm\n        do {\n            try realm.write {\n                for message in messagesToAdd {\n                    realm.add(message, update: .all)\n                }\n            }\n        } catch {\n            // 一般不会失败，真失败了算你小子运气差\n        }\n        \n        // 无论成功或失败，都删除已处理的 plist 文件\n        for plistUrl in urlsToDelete {\n            try? FileManager.default.removeItem(at: plistUrl)\n        }\n    }\n}\n"
  },
  {
    "path": "Bark/AppDelegate.swift",
    "content": "//\n//  AppDelegate.swift\n//  Bark\n//\n//  Created by huangfeng on 2018/3/7.\n//  Copyright © 2018年 Fin. All rights reserved.\n//\n\nimport IQKeyboardManagerSwift\nimport IQKeyboardToolbarManager\nimport SVProgressHUD\nimport SwiftyStoreKit\nimport UIKit\nimport UserNotifications\n\n@UIApplicationMain\nclass AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {\n    var window: UIWindow?\n\n    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {\n        self.window = UIWindow(frame: UIScreen.main.bounds)\n        self.window?.backgroundColor = UIColor.black\n\n        // 必须在应用一开始就配置，否则应用可能提前在配置之前试用了 Realm() ，则会创建两个独立数据库。\n        setupRealm()\n        \n        SVProgressHUD.setMinimumDismissTimeInterval(1.5)\n        \n        IQKeyboardManager.shared.isEnabled = true\n        IQKeyboardToolbarManager.shared.isEnabled = true\n        if #available(iOS 14, *), UIDevice.current.userInterfaceIdiom == .pad {\n            let splitViewController = BarkSplitViewController(style: .doubleColumn)\n            self.window?.rootViewController = BarkSnackbarController(rootViewController: splitViewController)\n        } else {\n            let tabBarController = BarkTabBarController()\n            self.window?.rootViewController = BarkSnackbarController(\n                rootViewController: tabBarController\n            )\n        }\n        \n        // 需先配置好 tabBarController 的 viewControllers，显示时会默认显示上次打开的页面\n        self.window?.makeKeyAndVisible()\n        \n        // 注册 Darwin Notification 监听\n        let notificationCenter = CFNotificationCenterGetDarwinNotifyCenter()\n        CFNotificationCenterAddObserver(\n            notificationCenter,\n            Unmanaged.passUnretained(self).toOpaque(),\n            { _, observer, _, _, _ in\n                guard let observer = observer else { return }\n                let appDelegate = Unmanaged<AppDelegate>.fromOpaque(observer).takeUnretainedValue()\n                DispatchQueue.main.async {\n                    appDelegate.processPendingMessages()\n                }\n            },\n            \"com.bark.newmessage\" as CFString,\n            nil,\n            .deliverImmediately\n        )\n        \n        // 处理可能在启动前积累的消息\n        processPendingMessages()\n        \n        UNUserNotificationCenter.current().delegate = self\n        var actions = [\n            UNNotificationAction(identifier: \"copy\", title: \"Copy2\".localized, options: UNNotificationActionOptions.foreground)\n        ]\n        if #available(iOSApplicationExtension 15.0, *) {\n            actions.append(UNNotificationAction(identifier: \"mute\", title: \"muteGroup1Hour\".localized, options: UNNotificationActionOptions.foreground))\n        }\n        UNUserNotificationCenter.current().setNotificationCategories([\n            // customDismissAction 会在 clear 推送时，调起APP，这时可以顺便更新下 DeviceToken，防止过期。\n            UNNotificationCategory(identifier: \"myNotificationCategory\", actions: actions, intentIdentifiers: [], options: .customDismissAction)\n        ])\n\n        UNUserNotificationCenter.current().getNotificationSettings { settings in\n            dispatch_sync_safely_main_queue {\n                if settings.authorizationStatus == .authorized {\n                    Client.shared.registerForRemoteNotifications()\n                }\n            }\n        }\n\n        // 调整返回按钮样式\n        let bar = UINavigationBar.appearance(whenContainedInInstancesOf: [BarkNavigationController.self])\n        bar.backIndicatorImage = UIImage(named: \"back\")\n        bar.backIndicatorTransitionMaskImage = UIImage(named: \"back\")\n        bar.tintColor = BKColor.grey.darken4\n\n        // 内购\n        SwiftyStoreKit.completeTransactions { _ in }\n        return true\n    }\n\n    func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {\n        print(error)\n    }\n\n    func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {\n        let deviceTokenString = deviceToken.map { String(format: \"%02.2hhx\", $0) }.joined()\n        Client.shared.deviceToken.accept(deviceTokenString)\n\n        // 注册设备\n        ServerManager.shared.syncAllServers()\n    }\n\n    func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {\n        guard response.actionIdentifier != UNNotificationDismissActionIdentifier else {\n            // clear 推送时，不要弹出提示框\n            return\n        }\n        notificatonHandler(userInfo: response.notification.request.content.userInfo)\n    }\n    func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {\n        guard let delete = userInfo[\"delete\"] as? String, delete == \"1\", let id = userInfo[\"id\"] as? String else {\n            completionHandler(.noData)\n            return\n        }\n        \n        // 删除通知中心推送\n        UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: [id])\n\n        // 删除历史记录\n        if let realm = try? Realm(),\n           let message = realm.objects(Message.self).filter(\"id == %@\", id).first\n        {\n            try? realm.write {\n                realm.delete(message)\n            }\n        }\n\n        completionHandler(.newData)\n    }\n    func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification) async -> UNNotificationPresentationOptions {\n        return .alert\n    }\n    \n    private func notificatonHandler(userInfo: [AnyHashable: Any]) {\n        let action = userInfo[\"action\"] as? String\n        if action == \"none\" {\n            return\n        }\n        \n        // 如果有 URL 直接打开\n        if let url = try? (userInfo[\"url\"] as? String)?.asURL() {\n            Client.shared.openUrl(url: url)\n            return\n        }\n        \n        if action == \"alert\" {\n            alertNotification(userInfo: userInfo)\n        }\n\n        // 跳转到消息列表tab\n        Client.shared.currentTabPage = .messageHistory\n    }\n\n    func applicationWillResignActive(_ application: UIApplication) {\n        // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.\n        // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.\n    }\n\n    func applicationDidEnterBackground(_ application: UIApplication) {\n        // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.\n        // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.\n    }\n\n    func applicationWillEnterForeground(_ application: UIApplication) {\n        // 设置 -1 可以清除应用角标，但不清除通知中心的推送\n        // 设置 0 会将通知中心的所有推送一起清空掉\n        UIApplication.shared.applicationIconBadgeNumber = -1\n        \n        // 处理待处理的消息\n        processPendingMessages()\n    }\n\n    func applicationWillTerminate(_ application: UIApplication) {\n        // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.\n    }\n    \n    func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {\n        if url.scheme?.lowercased() == \"bark\" && url.host?.lowercased() == \"addserver\" {\n            // 提取参数\n            let queryItems = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems\n            let address = queryItems?.first(where: { $0.name == \"address\" })?.value\n\n            // 处理添加服务器的逻辑\n            if let serverAddress = try? address?.asURL() {\n                let server = Server(address: serverAddress.absoluteString, key: \"\")\n                ServerManager.shared.addServer(server: server)\n                ServerManager.shared.setCurrentServer(serverId: server.id)\n                ServerManager.shared.syncAllServers()\n                HUDSuccess(\"AddedSuccessfully\".localized)\n            }\n            return true\n        }\n        return false\n    }\n}\n\nextension AppDelegate {\n    func alertNotification(userInfo: [AnyHashable: Any]) {\n        let alert = (userInfo[\"aps\"] as? [String: Any])?[\"alert\"] as? [String: Any]\n        let title = alert?[\"title\"] as? String\n        let subtitle = alert?[\"subtitle\"] as? String\n        let body = alert?[\"body\"] as? String\n        let url = try? (userInfo[\"url\"] as? String)?.asURL()\n\n        let alertController = UIAlertController(title: title, message: body, preferredStyle: .alert)\n        alertController.addAction(UIAlertAction(title: \"CopyContent\".localized, style: .default, handler: { _ in\n            if let copy = userInfo[\"copy\"] as? String {\n                UIPasteboard.general.string = copy\n            } else {\n                UIPasteboard.general.string = body\n            }\n        }))\n        alertController.addAction(UIAlertAction(title: \"MoreActions\".localized, style: .default, handler: { _ in\n            var shareContent = \"\"\n            if let title = title {\n                shareContent += \"\\(title)\\n\"\n            }\n            if let subtitle = subtitle {\n                shareContent += \"\\(subtitle)\\n\"\n            }\n            if let body = body {\n                shareContent += \"\\(body)\\n\"\n            }\n            for (key, value) in userInfo {\n                if [\"aps\", \"title\", \"subtitle\", \"body\", \"url\"].contains((key as? String) ?? \"\") {\n                    continue\n                }\n                shareContent += \"\\(key): \\(value) \\n\"\n            }\n            var items: [Any] = []\n            items.append(shareContent)\n            if let url = url {\n                items.append(url)\n            }\n            let controller = Client.shared.window?.rootViewController\n            let activityController = UIActivityViewController(activityItems: items,\n                                                              applicationActivities: nil)\n            if let popover = activityController.popoverPresentationController {\n                popover.sourceView = controller?.view\n                popover.sourceRect = CGRect(x: controller?.view.bounds.midX ?? 0, y: controller?.view.bounds.midY ?? 0, width: 0, height: 0)\n            }\n            controller?.present(activityController, animated: true, completion: nil)\n        }))\n        alertController.addAction(UIAlertAction(title: \"Cancel\".localized, style: .cancel, handler: nil))\n\n        let viewController = Client.shared.currentSnackbarController\n        \n        if let presentedController = viewController?.presentedViewController {\n            presentedController.dismiss(animated: false) {\n                viewController?.present(alertController, animated: true, completion: nil)\n            }\n        } else {\n            viewController?.present(alertController, animated: true, completion: nil)\n        }\n    }\n}\n"
  },
  {
    "path": "Bark/Assets.xcassets/AppIcon.appiconset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"filename\" : \"bark.png\",\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"size\" : \"1024x1024\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Bark/Assets.xcassets/Colors/Contents.json",
    "content": "{\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Bark/Assets.xcassets/Colors/background.colorset/Contents.json",
    "content": "{\n  \"colors\" : [\n    {\n      \"color\" : {\n        \"color-space\" : \"srgb\",\n        \"components\" : {\n          \"alpha\" : \"1.000\",\n          \"blue\" : \"245\",\n          \"green\" : \"245\",\n          \"red\" : \"245\"\n        }\n      },\n      \"idiom\" : \"universal\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"dark\"\n        }\n      ],\n      \"color\" : {\n        \"color-space\" : \"srgb\",\n        \"components\" : {\n          \"alpha\" : \"1.000\",\n          \"blue\" : \"0\",\n          \"green\" : \"0\",\n          \"red\" : \"0\"\n        }\n      },\n      \"idiom\" : \"universal\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Bark/Assets.xcassets/Colors/background_seconday.colorset/Contents.json",
    "content": "{\n  \"colors\" : [\n    {\n      \"color\" : {\n        \"color-space\" : \"srgb\",\n        \"components\" : {\n          \"alpha\" : \"1.000\",\n          \"blue\" : \"255\",\n          \"green\" : \"255\",\n          \"red\" : \"255\"\n        }\n      },\n      \"idiom\" : \"universal\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"dark\"\n        }\n      ],\n      \"color\" : {\n        \"color-space\" : \"srgb\",\n        \"components\" : {\n          \"alpha\" : \"1.000\",\n          \"blue\" : \"22\",\n          \"green\" : \"22\",\n          \"red\" : \"22\"\n        }\n      },\n      \"idiom\" : \"universal\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Bark/Assets.xcassets/Colors/black.colorset/Contents.json",
    "content": "{\n  \"colors\" : [\n    {\n      \"color\" : {\n        \"color-space\" : \"srgb\",\n        \"components\" : {\n          \"alpha\" : \"1.000\",\n          \"blue\" : \"0.000\",\n          \"green\" : \"0.000\",\n          \"red\" : \"0.000\"\n        }\n      },\n      \"idiom\" : \"universal\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"dark\"\n        }\n      ],\n      \"color\" : {\n        \"color-space\" : \"srgb\",\n        \"components\" : {\n          \"alpha\" : \"1.000\",\n          \"blue\" : \"1.000\",\n          \"green\" : \"1.000\",\n          \"red\" : \"1.000\"\n        }\n      },\n      \"idiom\" : \"universal\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Bark/Assets.xcassets/Colors/blue_base.colorset/Contents.json",
    "content": "{\n  \"colors\" : [\n    {\n      \"color\" : {\n        \"color-space\" : \"srgb\",\n        \"components\" : {\n          \"alpha\" : \"1.000\",\n          \"blue\" : \"243\",\n          \"green\" : \"150\",\n          \"red\" : \"33\"\n        }\n      },\n      \"idiom\" : \"universal\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"dark\"\n        }\n      ],\n      \"color\" : {\n        \"color-space\" : \"srgb\",\n        \"components\" : {\n          \"alpha\" : \"1.000\",\n          \"blue\" : \"243\",\n          \"green\" : \"150\",\n          \"red\" : \"33\"\n        }\n      },\n      \"idiom\" : \"universal\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Bark/Assets.xcassets/Colors/blue_darken1.colorset/Contents.json",
    "content": "{\n  \"colors\" : [\n    {\n      \"color\" : {\n        \"color-space\" : \"srgb\",\n        \"components\" : {\n          \"alpha\" : \"1.000\",\n          \"blue\" : \"229\",\n          \"green\" : \"136\",\n          \"red\" : \"30\"\n        }\n      },\n      \"idiom\" : \"universal\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"dark\"\n        }\n      ],\n      \"color\" : {\n        \"color-space\" : \"srgb\",\n        \"components\" : {\n          \"alpha\" : \"1.000\",\n          \"blue\" : \"245\",\n          \"green\" : \"165\",\n          \"red\" : \"66\"\n        }\n      },\n      \"idiom\" : \"universal\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Bark/Assets.xcassets/Colors/blue_darken5.colorset/Contents.json",
    "content": "{\n  \"colors\" : [\n    {\n      \"color\" : {\n        \"color-space\" : \"srgb\",\n        \"components\" : {\n          \"alpha\" : \"1.000\",\n          \"blue\" : \"216\",\n          \"green\" : \"46\",\n          \"red\" : \"34\"\n        }\n      },\n      \"idiom\" : \"universal\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"dark\"\n        }\n      ],\n      \"color\" : {\n        \"color-space\" : \"srgb\",\n        \"components\" : {\n          \"alpha\" : \"1.000\",\n          \"blue\" : \"233\",\n          \"green\" : \"44\",\n          \"red\" : \"70\"\n        }\n      },\n      \"idiom\" : \"universal\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Bark/Assets.xcassets/Colors/darkText_primary.colorset/Contents.json",
    "content": "{\n  \"colors\" : [\n    {\n      \"color\" : {\n        \"color-space\" : \"srgb\",\n        \"components\" : {\n          \"alpha\" : \"0.870\",\n          \"blue\" : \"255\",\n          \"green\" : \"255\",\n          \"red\" : \"255\"\n        }\n      },\n      \"idiom\" : \"universal\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"dark\"\n        }\n      ],\n      \"color\" : {\n        \"color-space\" : \"srgb\",\n        \"components\" : {\n          \"alpha\" : \"0.870\",\n          \"blue\" : \"0\",\n          \"green\" : \"0\",\n          \"red\" : \"0\"\n        }\n      },\n      \"idiom\" : \"universal\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Bark/Assets.xcassets/Colors/darkText_secondary.colorset/Contents.json",
    "content": "{\n  \"colors\" : [\n    {\n      \"color\" : {\n        \"color-space\" : \"srgb\",\n        \"components\" : {\n          \"alpha\" : \"0.540\",\n          \"blue\" : \"255\",\n          \"green\" : \"255\",\n          \"red\" : \"255\"\n        }\n      },\n      \"idiom\" : \"universal\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"dark\"\n        }\n      ],\n      \"color\" : {\n        \"color-space\" : \"srgb\",\n        \"components\" : {\n          \"alpha\" : \"0.540\",\n          \"blue\" : \"0\",\n          \"green\" : \"0\",\n          \"red\" : \"0\"\n        }\n      },\n      \"idiom\" : \"universal\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Bark/Assets.xcassets/Colors/grey_base.colorset/Contents.json",
    "content": "{\n  \"colors\" : [\n    {\n      \"color\" : {\n        \"color-space\" : \"srgb\",\n        \"components\" : {\n          \"alpha\" : \"1.000\",\n          \"blue\" : \"158\",\n          \"green\" : \"158\",\n          \"red\" : \"158\"\n        }\n      },\n      \"idiom\" : \"universal\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"dark\"\n        }\n      ],\n      \"color\" : {\n        \"color-space\" : \"srgb\",\n        \"components\" : {\n          \"alpha\" : \"1.000\",\n          \"blue\" : \"97\",\n          \"green\" : \"97\",\n          \"red\" : \"97\"\n        }\n      },\n      \"idiom\" : \"universal\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Bark/Assets.xcassets/Colors/grey_darken1.colorset/Contents.json",
    "content": "{\n  \"colors\" : [\n    {\n      \"color\" : {\n        \"color-space\" : \"srgb\",\n        \"components\" : {\n          \"alpha\" : \"1.000\",\n          \"blue\" : \"117\",\n          \"green\" : \"117\",\n          \"red\" : \"117\"\n        }\n      },\n      \"idiom\" : \"universal\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"dark\"\n        }\n      ],\n      \"color\" : {\n        \"color-space\" : \"srgb\",\n        \"components\" : {\n          \"alpha\" : \"1.000\",\n          \"blue\" : \"189\",\n          \"green\" : \"189\",\n          \"red\" : \"189\"\n        }\n      },\n      \"idiom\" : \"universal\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Bark/Assets.xcassets/Colors/grey_darken2.colorset/Contents.json",
    "content": "{\n  \"colors\" : [\n    {\n      \"color\" : {\n        \"color-space\" : \"srgb\",\n        \"components\" : {\n          \"alpha\" : \"1.000\",\n          \"blue\" : \"97\",\n          \"green\" : \"97\",\n          \"red\" : \"97\"\n        }\n      },\n      \"idiom\" : \"universal\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"dark\"\n        }\n      ],\n      \"color\" : {\n        \"color-space\" : \"srgb\",\n        \"components\" : {\n          \"alpha\" : \"1.000\",\n          \"blue\" : \"224\",\n          \"green\" : \"224\",\n          \"red\" : \"224\"\n        }\n      },\n      \"idiom\" : \"universal\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Bark/Assets.xcassets/Colors/grey_darken3.colorset/Contents.json",
    "content": "{\n  \"colors\" : [\n    {\n      \"color\" : {\n        \"color-space\" : \"srgb\",\n        \"components\" : {\n          \"alpha\" : \"1.000\",\n          \"blue\" : \"66\",\n          \"green\" : \"66\",\n          \"red\" : \"66\"\n        }\n      },\n      \"idiom\" : \"universal\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"dark\"\n        }\n      ],\n      \"color\" : {\n        \"color-space\" : \"srgb\",\n        \"components\" : {\n          \"alpha\" : \"1.000\",\n          \"blue\" : \"238\",\n          \"green\" : \"238\",\n          \"red\" : \"238\"\n        }\n      },\n      \"idiom\" : \"universal\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Bark/Assets.xcassets/Colors/grey_darken4.colorset/Contents.json",
    "content": "{\n  \"colors\" : [\n    {\n      \"color\" : {\n        \"color-space\" : \"srgb\",\n        \"components\" : {\n          \"alpha\" : \"1.000\",\n          \"blue\" : \"33\",\n          \"green\" : \"33\",\n          \"red\" : \"33\"\n        }\n      },\n      \"idiom\" : \"universal\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"dark\"\n        }\n      ],\n      \"color\" : {\n        \"color-space\" : \"srgb\",\n        \"components\" : {\n          \"alpha\" : \"1.000\",\n          \"blue\" : \"245\",\n          \"green\" : \"245\",\n          \"red\" : \"245\"\n        }\n      },\n      \"idiom\" : \"universal\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Bark/Assets.xcassets/Colors/grey_lighten1.colorset/Contents.json",
    "content": "{\n  \"colors\" : [\n    {\n      \"color\" : {\n        \"color-space\" : \"srgb\",\n        \"components\" : {\n          \"alpha\" : \"1.000\",\n          \"blue\" : \"189\",\n          \"green\" : \"189\",\n          \"red\" : \"189\"\n        }\n      },\n      \"idiom\" : \"universal\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"dark\"\n        }\n      ],\n      \"color\" : {\n        \"color-space\" : \"srgb\",\n        \"components\" : {\n          \"alpha\" : \"1.000\",\n          \"blue\" : \"117\",\n          \"green\" : \"117\",\n          \"red\" : \"117\"\n        }\n      },\n      \"idiom\" : \"universal\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Bark/Assets.xcassets/Colors/grey_lighten2.colorset/Contents.json",
    "content": "{\n  \"colors\" : [\n    {\n      \"color\" : {\n        \"color-space\" : \"srgb\",\n        \"components\" : {\n          \"alpha\" : \"1.000\",\n          \"blue\" : \"214\",\n          \"green\" : \"214\",\n          \"red\" : \"214\"\n        }\n      },\n      \"idiom\" : \"universal\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"dark\"\n        }\n      ],\n      \"color\" : {\n        \"color-space\" : \"srgb\",\n        \"components\" : {\n          \"alpha\" : \"1.000\",\n          \"blue\" : \"92\",\n          \"green\" : \"92\",\n          \"red\" : \"92\"\n        }\n      },\n      \"idiom\" : \"universal\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Bark/Assets.xcassets/Colors/grey_lighten3.colorset/Contents.json",
    "content": "{\n  \"colors\" : [\n    {\n      \"color\" : {\n        \"color-space\" : \"srgb\",\n        \"components\" : {\n          \"alpha\" : \"1.000\",\n          \"blue\" : \"238\",\n          \"green\" : \"238\",\n          \"red\" : \"238\"\n        }\n      },\n      \"idiom\" : \"universal\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"dark\"\n        }\n      ],\n      \"color\" : {\n        \"color-space\" : \"srgb\",\n        \"components\" : {\n          \"alpha\" : \"1.000\",\n          \"blue\" : \"66\",\n          \"green\" : \"66\",\n          \"red\" : \"66\"\n        }\n      },\n      \"idiom\" : \"universal\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Bark/Assets.xcassets/Colors/grey_lighten4.colorset/Contents.json",
    "content": "{\n  \"colors\" : [\n    {\n      \"color\" : {\n        \"color-space\" : \"srgb\",\n        \"components\" : {\n          \"alpha\" : \"1.000\",\n          \"blue\" : \"245\",\n          \"green\" : \"245\",\n          \"red\" : \"245\"\n        }\n      },\n      \"idiom\" : \"universal\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"dark\"\n        }\n      ],\n      \"color\" : {\n        \"color-space\" : \"srgb\",\n        \"components\" : {\n          \"alpha\" : \"1.000\",\n          \"blue\" : \"33\",\n          \"green\" : \"33\",\n          \"red\" : \"33\"\n        }\n      },\n      \"idiom\" : \"universal\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Bark/Assets.xcassets/Colors/grey_lighten5.colorset/Contents.json",
    "content": "{\n  \"colors\" : [\n    {\n      \"color\" : {\n        \"color-space\" : \"srgb\",\n        \"components\" : {\n          \"alpha\" : \"1.000\",\n          \"blue\" : \"250\",\n          \"green\" : \"250\",\n          \"red\" : \"250\"\n        }\n      },\n      \"idiom\" : \"universal\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"dark\"\n        }\n      ],\n      \"color\" : {\n        \"color-space\" : \"srgb\",\n        \"components\" : {\n          \"alpha\" : \"1.000\",\n          \"blue\" : \"28\",\n          \"green\" : \"28\",\n          \"red\" : \"28\"\n        }\n      },\n      \"idiom\" : \"universal\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Bark/Assets.xcassets/Colors/lightBlue_darken3.colorset/Contents.json",
    "content": "{\n  \"colors\" : [\n    {\n      \"color\" : {\n        \"color-space\" : \"srgb\",\n        \"components\" : {\n          \"alpha\" : \"1.000\",\n          \"blue\" : \"189\",\n          \"green\" : \"119\",\n          \"red\" : \"2\"\n        }\n      },\n      \"idiom\" : \"universal\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"dark\"\n        }\n      ],\n      \"color\" : {\n        \"color-space\" : \"srgb\",\n        \"components\" : {\n          \"alpha\" : \"1.000\",\n          \"blue\" : \"250\",\n          \"green\" : \"212\",\n          \"red\" : \"129\"\n        }\n      },\n      \"idiom\" : \"universal\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Bark/Assets.xcassets/Colors/notification_copy_color.colorset/Contents.json",
    "content": "{\n  \"colors\" : [\n    {\n      \"color\" : {\n        \"color-space\" : \"srgb\",\n        \"components\" : {\n          \"alpha\" : \"1.000\",\n          \"blue\" : \"0.000\",\n          \"green\" : \"0.000\",\n          \"red\" : \"0.000\"\n        }\n      },\n      \"idiom\" : \"universal\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"dark\"\n        }\n      ],\n      \"color\" : {\n        \"color-space\" : \"srgb\",\n        \"components\" : {\n          \"alpha\" : \"1.000\",\n          \"blue\" : \"1.000\",\n          \"green\" : \"1.000\",\n          \"red\" : \"1.000\"\n        }\n      },\n      \"idiom\" : \"universal\"\n    },\n    {\n      \"color\" : {\n        \"color-space\" : \"srgb\",\n        \"components\" : {\n          \"alpha\" : \"1.000\",\n          \"blue\" : \"0.000\",\n          \"green\" : \"0.000\",\n          \"red\" : \"0.000\"\n        }\n      },\n      \"idiom\" : \"iphone\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"dark\"\n        }\n      ],\n      \"color\" : {\n        \"color-space\" : \"srgb\",\n        \"components\" : {\n          \"alpha\" : \"1.000\",\n          \"blue\" : \"1.000\",\n          \"green\" : \"1.000\",\n          \"red\" : \"1.000\"\n        }\n      },\n      \"idiom\" : \"iphone\"\n    },\n    {\n      \"color\" : {\n        \"color-space\" : \"srgb\",\n        \"components\" : {\n          \"alpha\" : \"1.000\",\n          \"blue\" : \"0.000\",\n          \"green\" : \"0.000\",\n          \"red\" : \"0.000\"\n        }\n      },\n      \"idiom\" : \"ipad\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"dark\"\n        }\n      ],\n      \"color\" : {\n        \"color-space\" : \"srgb\",\n        \"components\" : {\n          \"alpha\" : \"1.000\",\n          \"blue\" : \"1.000\",\n          \"green\" : \"1.000\",\n          \"red\" : \"1.000\"\n        }\n      },\n      \"idiom\" : \"ipad\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Bark/Assets.xcassets/Colors/white.colorset/Contents.json",
    "content": "{\n  \"colors\" : [\n    {\n      \"color\" : {\n        \"color-space\" : \"srgb\",\n        \"components\" : {\n          \"alpha\" : \"1.000\",\n          \"blue\" : \"1.000\",\n          \"green\" : \"1.000\",\n          \"red\" : \"1.000\"\n        }\n      },\n      \"idiom\" : \"universal\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"dark\"\n        }\n      ],\n      \"color\" : {\n        \"color-space\" : \"srgb\",\n        \"components\" : {\n          \"alpha\" : \"1.000\",\n          \"blue\" : \"0.000\",\n          \"green\" : \"0.000\",\n          \"red\" : \"0.000\"\n        }\n      },\n      \"idiom\" : \"universal\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Bark/Assets.xcassets/Contents.json",
    "content": "{\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Bark/Assets.xcassets/back.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"idiom\" : \"universal\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"idiom\" : \"universal\",\n      \"filename\" : \"back@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"idiom\" : \"universal\",\n      \"filename\" : \"back@3x.png\",\n      \"scale\" : \"3x\"\n    }\n  ],\n  \"info\" : {\n    \"version\" : 1,\n    \"author\" : \"xcode\"\n  }\n}"
  },
  {
    "path": "Bark/Assets.xcassets/baseline_check_circle_outline_black_24pt.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"filename\" : \"baseline_check_circle_outline_black_24pt_1x.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"filename\" : \"baseline_check_circle_outline_black_24pt_2x.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"filename\" : \"baseline_check_circle_outline_black_24pt_3x.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"3x\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  },\n  \"properties\" : {\n    \"template-rendering-intent\" : \"template\"\n  }\n}\n"
  },
  {
    "path": "Bark/Assets.xcassets/baseline_close_white_48pt.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"filename\" : \"baseline_close_white_48pt_1x.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"filename\" : \"baseline_close_white_48pt_2x.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"filename\" : \"baseline_close_white_48pt_3x.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"3x\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  },\n  \"properties\" : {\n    \"template-rendering-intent\" : \"template\"\n  }\n}\n"
  },
  {
    "path": "Bark/Assets.xcassets/baseline_delete_outline_black_24pt.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"filename\" : \"baseline_delete_outline_black_24pt_1x.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"filename\" : \"baseline_delete_outline_black_24pt_2x.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"filename\" : \"baseline_delete_outline_black_24pt_3x.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"3x\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  },\n  \"properties\" : {\n    \"template-rendering-intent\" : \"template\"\n  }\n}\n"
  },
  {
    "path": "Bark/Assets.xcassets/baseline_file_copy_white_24pt.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"idiom\" : \"universal\",\n      \"filename\" : \"baseline_file_copy_white_24pt_1x.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"idiom\" : \"universal\",\n      \"filename\" : \"baseline_file_copy_white_24pt_2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"idiom\" : \"universal\",\n      \"filename\" : \"baseline_file_copy_white_24pt_3x.png\",\n      \"scale\" : \"3x\"\n    }\n  ],\n  \"info\" : {\n    \"version\" : 1,\n    \"author\" : \"xcode\"\n  },\n  \"properties\" : {\n    \"template-rendering-intent\" : \"template\"\n  }\n}"
  },
  {
    "path": "Bark/Assets.xcassets/baseline_filter_drama_black_24pt.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"filename\" : \"baseline_filter_drama_black_24pt_1x.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"filename\" : \"baseline_filter_drama_black_24pt_2x.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"filename\" : \"baseline_filter_drama_black_24pt_3x.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"3x\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  },\n  \"properties\" : {\n    \"template-rendering-intent\" : \"template\"\n  }\n}\n"
  },
  {
    "path": "Bark/Assets.xcassets/baseline_folder_open_black_24pt.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"filename\" : \"baseline_folder_open_black_24pt_1x.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"filename\" : \"baseline_folder_open_black_24pt_2x.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"filename\" : \"baseline_folder_open_black_24pt_3x.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"3x\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  },\n  \"properties\" : {\n    \"template-rendering-intent\" : \"template\"\n  }\n}\n"
  },
  {
    "path": "Bark/Assets.xcassets/baseline_gite_black_24pt.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"filename\" : \"baseline_gite_black_24pt_1x.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"filename\" : \"baseline_gite_black_24pt_2x.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"filename\" : \"baseline_gite_black_24pt_3x.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"3x\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  },\n  \"properties\" : {\n    \"template-rendering-intent\" : \"template\"\n  }\n}\n"
  },
  {
    "path": "Bark/Assets.xcassets/baseline_http_black_24pt.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"idiom\" : \"universal\",\n      \"filename\" : \"baseline_http_black_24pt_1x.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"idiom\" : \"universal\",\n      \"filename\" : \"baseline_http_black_24pt_2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"idiom\" : \"universal\",\n      \"filename\" : \"baseline_http_black_24pt_3x.png\",\n      \"scale\" : \"3x\"\n    }\n  ],\n  \"info\" : {\n    \"version\" : 1,\n    \"author\" : \"xcode\"\n  },\n  \"properties\" : {\n    \"template-rendering-intent\" : \"template\"\n  }\n}"
  },
  {
    "path": "Bark/Assets.xcassets/baseline_https_black_24pt.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"idiom\" : \"universal\",\n      \"filename\" : \"baseline_https_black_24pt_1x.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"idiom\" : \"universal\",\n      \"filename\" : \"baseline_https_black_24pt_2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"idiom\" : \"universal\",\n      \"filename\" : \"baseline_https_black_24pt_3x.png\",\n      \"scale\" : \"3x\"\n    }\n  ],\n  \"info\" : {\n    \"version\" : 1,\n    \"author\" : \"xcode\"\n  },\n  \"properties\" : {\n    \"template-rendering-intent\" : \"template\"\n  }\n}"
  },
  {
    "path": "Bark/Assets.xcassets/baseline_keyboard_arrow_down_black_24pt.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"filename\" : \"baseline_keyboard_arrow_down_black_24pt_1x.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"filename\" : \"baseline_keyboard_arrow_down_black_24pt_2x.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"filename\" : \"baseline_keyboard_arrow_down_black_24pt_3x.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"3x\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  },\n  \"properties\" : {\n    \"template-rendering-intent\" : \"template\"\n  }\n}\n"
  },
  {
    "path": "Bark/Assets.xcassets/baseline_manage_accounts_black_24pt.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"filename\" : \"baseline_manage_accounts_black_24pt_1x.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"filename\" : \"baseline_manage_accounts_black_24pt_2x.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"filename\" : \"baseline_manage_accounts_black_24pt_3x.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"3x\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  },\n  \"properties\" : {\n    \"template-rendering-intent\" : \"template\"\n  }\n}\n"
  },
  {
    "path": "Bark/Assets.xcassets/baseline_qr_code_scanner_black_24pt.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"filename\" : \"baseline_qr_code_scanner_black_24pt_1x.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"filename\" : \"baseline_qr_code_scanner_black_24pt_2x.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"filename\" : \"baseline_qr_code_scanner_black_24pt_3x.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"3x\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  },\n  \"properties\" : {\n    \"template-rendering-intent\" : \"template\"\n  }\n}\n"
  },
  {
    "path": "Bark/Assets.xcassets/baseline_radio_button_unchecked_black_24pt.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"filename\" : \"baseline_radio_button_unchecked_black_24pt_1x.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"filename\" : \"baseline_radio_button_unchecked_black_24pt_2x.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"filename\" : \"baseline_radio_button_unchecked_black_24pt_3x.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"3x\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  },\n  \"properties\" : {\n    \"template-rendering-intent\" : \"template\"\n  }\n}\n"
  },
  {
    "path": "Bark/Assets.xcassets/baseline_remove_black_20pt.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"filename\" : \"baseline_remove_black_20pt_1x.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"filename\" : \"baseline_remove_black_20pt_2x.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"filename\" : \"baseline_remove_black_20pt_3x.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"3x\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  },\n  \"properties\" : {\n    \"template-rendering-intent\" : \"template\"\n  }\n}\n"
  },
  {
    "path": "Bark/Assets.xcassets/baseline_wifi_black_20pt.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"filename\" : \"baseline_wifi_black_20pt_1x.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"filename\" : \"baseline_wifi_black_20pt_2x.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"filename\" : \"baseline_wifi_black_20pt_3x.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"3x\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  },\n  \"properties\" : {\n    \"template-rendering-intent\" : \"template\"\n  }\n}\n"
  },
  {
    "path": "Bark/Assets.xcassets/baseline_wifi_off_black_20pt.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"filename\" : \"baseline_wifi_off_black_20pt_1x.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"filename\" : \"baseline_wifi_off_black_20pt_2x.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"filename\" : \"baseline_wifi_off_black_20pt_3x.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"3x\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  },\n  \"properties\" : {\n    \"template-rendering-intent\" : \"template\"\n  }\n}\n"
  },
  {
    "path": "Bark/Assets.xcassets/copyTest.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"idiom\" : \"universal\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"dark\"\n        }\n      ],\n      \"idiom\" : \"universal\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"idiom\" : \"universal\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"dark\"\n        }\n      ],\n      \"idiom\" : \"universal\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"filename\" : \"preview_copy@3x.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"3x\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"dark\"\n        }\n      ],\n      \"filename\" : \"preview_copy_dark@3x.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"3x\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Bark/Assets.xcassets/criticalAlert.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"idiom\" : \"universal\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"dark\"\n        }\n      ],\n      \"idiom\" : \"universal\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"idiom\" : \"universal\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"dark\"\n        }\n      ],\n      \"idiom\" : \"universal\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"filename\" : \"IMG_8023.jpg\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"3x\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"dark\"\n        }\n      ],\n      \"filename\" : \"IMG_8024.jpg\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"3x\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Bark/Assets.xcassets/group.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"idiom\" : \"universal\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"dark\"\n        }\n      ],\n      \"idiom\" : \"universal\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"idiom\" : \"universal\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"dark\"\n        }\n      ],\n      \"idiom\" : \"universal\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"filename\" : \"preview_group_default@3x.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"3x\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"dark\"\n        }\n      ],\n      \"filename\" : \"preview_group_dark@3x.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"3x\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Bark/Assets.xcassets/group_collapse.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"idiom\" : \"universal\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"filename\" : \"folder_code_24dp_5F6368_FILL0_wght400_GRAD0_opsz242 (3).png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"filename\" : \"folder_code_24dp_5F6368_FILL0_wght400_GRAD0_opsz242 (2).png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"3x\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Bark/Assets.xcassets/group_expand.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"idiom\" : \"universal\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"filename\" : \"folder_code_24dp_5F6368_FILL0_wght400_GRAD0_opsz24 1.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"filename\" : \"folder_code_24dp_5F6368_FILL0_wght400_GRAD0_opsz24.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"3x\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Bark/Assets.xcassets/icon.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"idiom\" : \"universal\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"dark\"\n        }\n      ],\n      \"idiom\" : \"universal\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"idiom\" : \"universal\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"dark\"\n        }\n      ],\n      \"idiom\" : \"universal\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"filename\" : \"preview_icon_default@3x.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"3x\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"dark\"\n        }\n      ],\n      \"filename\" : \"preview_icon_dark@3x.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"3x\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Bark/Assets.xcassets/keyboard_arrow_right_symbol.symbolset/Contents.json",
    "content": "{\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  },\n  \"symbols\" : [\n    {\n      \"filename\" : \"keyboard_arrow_right_keyboard_arrow_right_symbol.svg\",\n      \"idiom\" : \"universal\"\n    }\n  ]\n}\n"
  },
  {
    "path": "Bark/Assets.xcassets/music_note-music_note_symbol.symbolset/Contents.json",
    "content": "{\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  },\n  \"symbols\" : [\n    {\n      \"filename\" : \"music_note-music_note_symbol.svg\",\n      \"idiom\" : \"universal\"\n    }\n  ]\n}\n"
  },
  {
    "path": "Bark/Assets.xcassets/offline.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"idiom\" : \"universal\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"idiom\" : \"universal\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"filename\" : \"offline@3x.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"3x\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Bark/Assets.xcassets/online.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"idiom\" : \"universal\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"idiom\" : \"universal\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"filename\" : \"online@3X.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"3x\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Bark/Assets.xcassets/warning.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"idiom\" : \"universal\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"idiom\" : \"universal\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"filename\" : \"warning@3x.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"3x\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Bark/Bark.entitlements",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>aps-environment</key>\n\t<string>development</string>\n\t<key>com.apple.developer.usernotifications.communication</key>\n\t<true/>\n\t<key>com.apple.developer.usernotifications.critical-alerts</key>\n\t<true/>\n\t<key>com.apple.developer.usernotifications.time-sensitive</key>\n\t<true/>\n\t<key>com.apple.security.application-groups</key>\n\t<array>\n\t\t<string>group.bark</string>\n\t</array>\n</dict>\n</plist>\n"
  },
  {
    "path": "Bark/Base.lproj/LaunchScreen.storyboard",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<document type=\"com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB\" version=\"3.0\" toolsVersion=\"19162\" targetRuntime=\"iOS.CocoaTouch\" propertyAccessControl=\"none\" useAutolayout=\"YES\" launchScreen=\"YES\" useTraitCollections=\"YES\" useSafeAreas=\"YES\" colorMatched=\"YES\" initialViewController=\"01J-lp-oVM\">\n    <device id=\"retina6_1\" orientation=\"portrait\" appearance=\"light\"/>\n    <dependencies>\n        <deployment identifier=\"iOS\"/>\n        <plugIn identifier=\"com.apple.InterfaceBuilder.IBCocoaTouchPlugin\" version=\"19144\"/>\n        <capability name=\"Named colors\" minToolsVersion=\"9.0\"/>\n        <capability name=\"Safe area layout guides\" minToolsVersion=\"9.0\"/>\n        <capability name=\"documents saved in the Xcode 8 format\" minToolsVersion=\"8.0\"/>\n    </dependencies>\n    <scenes>\n        <!--View Controller-->\n        <scene sceneID=\"EHf-IW-A2E\">\n            <objects>\n                <viewController id=\"01J-lp-oVM\" sceneMemberID=\"viewController\">\n                    <view key=\"view\" contentMode=\"scaleToFill\" id=\"Ze5-6b-2t3\">\n                        <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"414\" height=\"896\"/>\n                        <autoresizingMask key=\"autoresizingMask\" widthSizable=\"YES\" heightSizable=\"YES\"/>\n                        <viewLayoutGuide key=\"safeArea\" id=\"6Tk-OE-BBY\"/>\n                        <color key=\"backgroundColor\" name=\"background\"/>\n                    </view>\n                </viewController>\n                <placeholder placeholderIdentifier=\"IBFirstResponder\" id=\"iYj-Kq-Ea1\" userLabel=\"First Responder\" sceneMemberID=\"firstResponder\"/>\n            </objects>\n            <point key=\"canvasLocation\" x=\"53\" y=\"375\"/>\n        </scene>\n    </scenes>\n    <resources>\n        <namedColor name=\"background\">\n            <color red=\"0.96078431372549022\" green=\"0.96078431372549022\" blue=\"0.96078431372549022\" alpha=\"1\" colorSpace=\"custom\" customColorSpace=\"sRGB\"/>\n        </namedColor>\n    </resources>\n</document>\n"
  },
  {
    "path": "Bark/Info.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>NSPhotoLibraryAddUsageDescription</key>\n\t<string>Save the image to the photo library</string>\n\t<key>CFBundleURLTypes</key>\n\t<array>\n\t\t<dict>\n\t\t\t<key>CFBundleURLSchemes</key>\n\t\t\t<array>\n\t\t\t\t<string>bark</string>\n\t\t\t</array>\n\t\t\t<key>CFBundleURLName</key>\n\t\t\t<string>me.fin.bark</string>\n\t\t</dict>\n\t</array>\n\t<key>CFBundleDevelopmentRegion</key>\n\t<string>$(DEVELOPMENT_LANGUAGE)</string>\n\t<key>CFBundleDisplayName</key>\n\t<string>Bark</string>\n\t<key>CFBundleExecutable</key>\n\t<string>$(EXECUTABLE_NAME)</string>\n\t<key>CFBundleIdentifier</key>\n\t<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>\n\t<key>CFBundleInfoDictionaryVersion</key>\n\t<string>6.0</string>\n\t<key>CFBundleName</key>\n\t<string>$(PRODUCT_NAME)</string>\n\t<key>CFBundlePackageType</key>\n\t<string>APPL</string>\n\t<key>CFBundleShortVersionString</key>\n\t<string>$(MARKETING_VERSION)</string>\n\t<key>CFBundleVersion</key>\n\t<string>$(CURRENT_PROJECT_VERSION)</string>\n\t<key>GitHub Run Id</key>\n\t<string>0</string>\n\t<key>LSRequiresIPhoneOS</key>\n\t<true/>\n\t<key>NSAppTransportSecurity</key>\n\t<dict>\n\t\t<key>NSAllowsArbitraryLoads</key>\n\t\t<true/>\n\t</dict>\n\t<key>NSCameraUsageDescription</key>\n\t<string>We are using the camera to scan a QR code</string>\n\t<key>NSUserActivityTypes</key>\n\t<array>\n\t\t<string>INSendMessageIntent</string>\n\t\t<string>INStartCallIntent</string>\n\t</array>\n\t<key>UIBackgroundModes</key>\n\t<array>\n\t\t<string>remote-notification</string>\n\t</array>\n\t<key>UILaunchStoryboardName</key>\n\t<string>LaunchScreen</string>\n\t<key>UIRequiredDeviceCapabilities</key>\n\t<array>\n\t\t<string>armv7</string>\n\t</array>\n\t<key>UISupportedInterfaceOrientations</key>\n\t<array>\n\t\t<string>UIInterfaceOrientationPortrait</string>\n\t</array>\n\t<key>UISupportedInterfaceOrientations~ipad</key>\n\t<array>\n\t\t<string>UIInterfaceOrientationPortrait</string>\n\t\t<string>UIInterfaceOrientationPortraitUpsideDown</string>\n\t\t<string>UIInterfaceOrientationLandscapeLeft</string>\n\t\t<string>UIInterfaceOrientationLandscapeRight</string>\n\t</array>\n\t<key>UIViewControllerBasedStatusBarAppearance</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "Bark/Intents/OptionsProvider.swift",
    "content": "//\n//  OptionsProvider.swift\n//  Bark\n//\n//  Created by huangfeng on 2/21/25.\n//  Copyright © 2025 Fin. All rights reserved.\n//\nimport AppIntents\n\n@available(iOS 16, *)\nstruct ServerAddressOptionsProvider: DynamicOptionsProvider {\n    func results() async throws -> [String] {\n        return ServerManager.shared.servers.map { server in\n            return server.address + \"/\" + server.key\n        }\n    }\n\n    func defaultResult() async -> String? {\n        return ServerManager.shared.currentServer.address + \"/\" + ServerManager.shared.currentServer.key\n    }\n}\n\n@available(iOS 16, *)\nstruct SoundOptionsProvider: DynamicOptionsProvider {\n    func results() async throws -> [String] {\n        var customSounds: [String] = []\n        if let soundsDirectoryUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: \"group.bark\")?.appendingPathComponent(\"Library/Sounds\").path {\n            customSounds = getSounds(urls: getFilesInDirectory(directory: soundsDirectoryUrl, suffix: \"caf\"))\n        }\n        let defaultSounds = getSounds(urls: Bundle.main.urls(forResourcesWithExtension: \"caf\", subdirectory: nil) ?? [])\n        \n        return customSounds + defaultSounds\n    }\n    \n    func getSounds(urls: [URL]) -> [String] {\n        let urls = urls.sorted { u1, u2 -> Bool in\n            u1.lastPathComponent.localizedStandardCompare(u2.lastPathComponent) == ComparisonResult.orderedAscending\n        }\n        return urls.map { $0.deletingPathExtension().lastPathComponent }\n    }\n    \n    func getFilesInDirectory(directory: String, suffix: String) -> [URL] {\n        let fileManager = FileManager.default\n        do {\n            let files = try fileManager.contentsOfDirectory(atPath: directory)\n            return files.compactMap { file -> URL? in\n                if file.hasSuffix(suffix), !file.hasPrefix(kBarkSoundPrefix) {\n                    // 不要包含 kBarkSoundPrefix 开头的，这些是为了 call=1 合成的 30s 长铃声,不算用户上传的\n                    return URL(fileURLWithPath: directory).appendingPathComponent(file)\n                }\n                return nil\n            }\n        } catch {\n            return []\n        }\n    }\n}\n\n@available(iOS 16, *)\nstruct VolumeOptionsProvider: DynamicOptionsProvider {\n    func results() async throws -> [Int] {\n        return Array(0...10)\n    }\n}\n"
  },
  {
    "path": "Bark/Intents/PushResponse.swift",
    "content": "//\n//  PushResponse.swift\n//  Bark\n//\n//  Created by huangfeng on 2/21/25.\n//  Copyright © 2025 Fin. All rights reserved.\n//\n\nstruct PushResponse: Decodable {\n    let message: String?\n    let code: Int?\n}\n"
  },
  {
    "path": "Bark/Intents/PushToCurrentIntent.swift",
    "content": "//\n//  Intents.swift\n//  Bark\n//\n//  Created by huangfeng on 2/20/25.\n//  Copyright © 2025 Fin. All rights reserved.\n//\n\nimport Alamofire\nimport AppIntents\n\n@available(iOS 16, *)\nstruct PushToCurrentIntent: AppIntent {\n    static var title: LocalizedStringResource = \"sendPushNotification\"\n    static var openAppWhenRun: Bool = false\n\n    @Parameter(title: \"ServerAddress\", optionsProvider: ServerAddressOptionsProvider())\n    var address: String\n    \n    @Parameter(title: \"CustomedNotificationTitle\")\n    var title: String?\n    @Parameter(title: \"CustomedNotificationContent\")\n    var body: String?\n\n    @Parameter(title: \"ringtone\")\n    var isCall: Bool\n    \n    @Parameter(title: \"criticalAlert\")\n    var isCritical: Bool\n    \n    @Parameter(title: \"ringtoneVolume\", optionsProvider: VolumeOptionsProvider())\n    var volume: Int?\n    \n    @Parameter(title: \"notificationSound\", optionsProvider: SoundOptionsProvider())\n    var sound: String?\n    \n    @Parameter(title: \"notificationIcon\")\n    var icon: URL?\n    \n    @Parameter(title: \"group\")\n    var group: String?\n    \n    func perform() async throws -> some IntentResult & ReturnsValue<Bool> {\n        guard let address = URL(string: address) else {\n            throw \"Invalid URL\"\n        }\n        \n        var params: [String: Any] = [:]\n        \n        if let title, !title.isEmpty {\n            params[\"title\"] = title.urlDecoded()\n        }\n        if let body, !body.isEmpty {\n            params[\"body\"] = body.urlDecoded()\n        }\n        if title == nil, body == nil {\n            params[\"body\"] = \"Empty Notification\"\n        }\n        if isCritical {\n            params[\"level\"] = \"critical\"\n        }\n        if let volume {\n            params[\"volume\"] = volume\n        }\n        if isCall {\n            params[\"call\"] = 1\n        }\n        if let sound, !sound.isEmpty {\n            params[\"sound\"] = sound\n        }\n        if let icon {\n            params[\"icon\"] = icon.absoluteString\n        }\n        if let group, !group.isEmpty {\n            params[\"group\"] = group\n        }\n        \n        let response = await AF.request(address, method: .post, parameters: params, encoding: JSONEncoding.default)\n            .serializingDecodable(PushResponse.self)\n            .response\n        \n        // 打印返回的body\n        if response.response?.statusCode != 200 {\n            return .result(value: false)\n        }\n        return .result(value: true)\n    }\n}\n"
  },
  {
    "path": "Bark/Intents/PushToOtherIntent.swift",
    "content": "//\n//  PushToOtherIntent.swift\n//  Bark\n//\n//  Created by huangfeng on 2/21/25.\n//  Copyright © 2025 Fin. All rights reserved.\n//\nimport Alamofire\nimport AppIntents\n\n@available(iOS 16, *)\nstruct PushToOtherIntent: AppIntent {\n    static var title: LocalizedStringResource = \"sendPushNotificationToOther\"\n    static var openAppWhenRun: Bool = false\n\n    @Parameter(title: \"ServerAddress\")\n    var address: String\n    \n    @Parameter(title: \"CustomedNotificationTitle\")\n    var title: String?\n    @Parameter(title: \"CustomedNotificationContent\")\n    var body: String?\n\n    @Parameter(title: \"ringtone\")\n    var isCall: Bool\n    \n    @Parameter(title: \"criticalAlert\")\n    var isCritical: Bool\n    \n    @Parameter(title: \"ringtoneVolume\", optionsProvider: VolumeOptionsProvider())\n    var volume: Int?\n    \n    @Parameter(title: \"notificationSound\", optionsProvider: SoundOptionsProvider())\n    var sound: String?\n    \n    @Parameter(title: \"notificationIcon\")\n    var icon: URL?\n    \n    @Parameter(title: \"group\")\n    var group: String?\n    \n    func perform() async throws -> some IntentResult & ReturnsValue<Bool> {\n        guard let address = URL(string: address) else {\n            throw \"Invalid URL\"\n        }\n        \n        var params: [String: Any] = [:]\n        \n        if let title, !title.isEmpty {\n            params[\"title\"] = title.urlDecoded()\n        }\n        if let body, !body.isEmpty {\n            params[\"body\"] = body.urlDecoded()\n        }\n        if title == nil, body == nil {\n            params[\"body\"] = \"Empty Notification\"\n        }\n        if isCritical {\n            params[\"level\"] = \"critical\"\n        }\n        if let volume {\n            params[\"volume\"] = volume\n        }\n        if isCall {\n            params[\"call\"] = 1\n        }\n        if let sound, !sound.isEmpty {\n            params[\"sound\"] = sound\n        }\n        if let icon {\n            params[\"icon\"] = icon.absoluteString\n        }\n        if let group, !group.isEmpty {\n            params[\"group\"] = group\n        }\n        \n        let response = await AF.request(address, method: .post, parameters: params, encoding: JSONEncoding.default)\n            .serializingDecodable(PushResponse.self)\n            .response\n        \n        // 打印返回的body\n        if response.response?.statusCode != 200 {\n            return .result(value: false)\n        }\n        return .result(value: true)\n    }\n}\n"
  },
  {
    "path": "Bark/Localizable.xcstrings",
    "content": "{\n  \"sourceLanguage\" : \"en\",\n  \"strings\" : {\n    \"AddedSuccessfully\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Server added successfully.\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"サーバーが正常に追加されました。\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Sunucu başarıyla eklendi.\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"添加成功！\"\n          }\n        }\n      }\n    },\n    \"AddServer\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Add a Server\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"サーバーを追加\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Sunucu Ekleme\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"添加私有服务器\"\n          }\n        }\n      }\n    },\n    \"algorithm\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Algorithm\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"アルゴリズム\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Algoritma\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"算法\"\n          }\n        }\n      }\n    },\n    \"AllowNotifications\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Notifications have been turned off. Please allow notifications in the Settings app.\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"通知がオフになっています。設定アプリで通知を許可してください。\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Bildirimler kapatıldı. Lütfen Ayarlar uygulamasından bildirimlere izin verin.\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"绑定设备需要推送。请打开推送后重试\"\n          }\n        }\n      }\n    },\n    \"allTime\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"All time\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"全期間\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Tüm zamanlar\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"所有时间\"\n          }\n        }\n      }\n    },\n    \"archiveNote\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Archive notifications by default if the 'isArchive' parameter is unset.\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"'isArchive'パラメータが未設定の場合、デフォルトで通知をアーカイブします。\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"'isArchive' parametresi ayarlanmamışsa varsayılan olarak bildirimleri arşivleyin.\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"当推送请求URL没有指定 isArchive 参数时，将按照此设置来决定是否保存通知消息\"\n          }\n        }\n      }\n    },\n    \"archiveNotificationMessage\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Archive a notification by passing 'isArchive=1'. Prevent archiving by passing 'isArchive=0'. Default settings will apply if the parameter is unset.\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"'isArchive=1'を渡して通知をアーカイブします。'isArchive=0'を渡してアーカイブを防止します。パラメータが未設定の場合、デフォルト設定が適用されます。\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"'isArchive=1' değerini geçerek bir bildirimi arşivleyin. 'isArchive=0' geçerek arşivlemeyi önleyin. Parametre ayarlanmamışsa varsayılan ayarlar geçerli olacaktır.\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"当设置 isArchive 值为 1 时，则会自动保存这条推送消息，设置为其他值时，则不会保存。\\n如果不指定 isArchive 参数时，则按照默认设置保存消息，可以在 设置->默认保存消息 更改默认保存设置\"\n          }\n        }\n      }\n    },\n    \"archiveNotificationMessageTitle\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Archive Notification\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"通知をアーカイブ\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Arşiv Bildirimi\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"自动保存通知消息\"\n          }\n        }\n      }\n    },\n    \"automaticallyCopy\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Automatically copy the body text of a notification, only for iOS 14.4 and below. For iOS 14.5 and above, long press or pull down the notification to copy.\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"通知の本文を自動的にコピーします（iOS 14.4以下のみ）。iOS 14.5以上では、通知を長押しまたは引き下げてコピーします。\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Bir bildirimin gövde metnini otomatik olarak kopyalayın, yalnızca iOS 14.4 ve altı için. iOS 14.5 ve üzeri sürümlerde, kopyalamak için bildirime uzun basın veya aşağı çekin.\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"携带automaticallyCopy参数时，将自动复制推送内容。\\n携带copy参数时，将只复制copy参数的值。\\niOS14.5 之后长按或下拉推送即可触发自动复制，iOS14.5之前无需任何操作即可自动复制。\"\n          }\n        }\n      }\n    },\n    \"automaticallyCopyTitle\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Auto Copy Body Text (iOS 14.4 and below)\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"本文を自動コピー（iOS 14.4以下）\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Gövde Metnini Otomatik Kopyala (iOS 14.4 ve altı)\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"自动复制推送内容到剪切板\"\n          }\n        }\n      }\n    },\n    \"badge\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Badge Count\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"バッジカウント\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Rozet Sayısı\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"设置角标\"\n          }\n        }\n      }\n    },\n    \"badgeNotice\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Specify a number on Bark app’s icon when a notification arrives.\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"通知が到着したときにBarkアプリのアイコンに数字を指定します。\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Bir bildirim geldiğinde Bark uygulamasının simgesinde bir sayı belirtin.\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"可以为推送设置角标，角标可以是任意数字。\"\n          }\n        }\n      }\n    },\n    \"base64Notice\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"If you get a 'Decryption Failed' prompt, try adding '-w 0' after the base64 command.\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"「復号に失敗しました」というプロンプトが表示された場合、base64コマンドの後に「-w 0」を追加してみてください。\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"şifre çözme başarısız olduysa, base64 komutundan sonra '-i 0' eklemeyi deneyin.\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"如果提示 Decryption Failed， 尝试在 base64 命令后面 加上 -w 0\"\n          }\n        }\n      }\n    },\n    \"beforeAMonth\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Before a month\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"1ヶ月前\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Bir Ay Önce\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"一个月之前\"\n          }\n        }\n      }\n    },\n    \"beforeAnHour\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Before an hour\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"1時間前\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Bir Saat Önce\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"一小时之前\"\n          }\n        }\n      }\n    },\n    \"beforeToday\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Before today\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"今日より前\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Bugünden Önce\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"今天之前\"\n          }\n        }\n      }\n    },\n    \"beforeYesterday\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Before yesterday\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"昨日より前\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Dünden Önce\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"昨天之前\"\n          }\n        }\n      }\n    },\n    \"buildDesc\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Bark uses GitHub Actions to build and publish itself to the App Store. You can verify the ‘run_id’ in the URL to ensure Bark was built from its original open-source codebase.\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"BarkはGitHub Actionsを使用して自身をビルドし、App Storeに公開します。URL内の「run_id」を確認して、Barkがオリジナルのオープンソースコードベースからビルドされたことを確認できます。\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Bark, kendisini oluşturmak ve App Store'da yayınlamak için GitHub Actions'ı kullanır. Bark'ın orijinal açık kaynak kod tabanından oluşturulduğundan emin olmak için URL'deki 'run_id'yi doğrulayabilirsiniz.\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Bark 由 GitHub Actions 构建后上传到 App Store，此信息可帮助你确认当前版本是由开源代码所构建，未经任何人(包含作者)修改。\"\n          }\n        }\n      }\n    },\n    \"Cancel\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Cancel\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"キャンセル\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Vazgeç\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"取消\"\n          }\n        }\n      }\n    },\n    \"ciphertextComment\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"URL encoding the ciphertext, there may be special characters.\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"暗号文をURLエンコードします。特殊文字が含まれる場合があります。\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"URL şifreli metni kodlarken özel karakterler olabilir.\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"密文可能有特殊字符，所以记得 URL 编码一下。\"\n          }\n        }\n      }\n    },\n    \"clear\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Clear\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"クリア\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Temizle\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"清除\"\n          }\n        }\n      }\n    },\n    \"clearFrom\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Clear from:\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"クリア範囲:\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Şuradan Temizle:\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"清除以下时间段的历史消息:\"\n          }\n        }\n      }\n    },\n    \"close\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Close\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"閉じる\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Kapat\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"关闭\"\n          }\n        }\n      }\n    },\n    \"confirm\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Confirm\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"確認\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Onayla\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"确定\"\n          }\n        }\n      }\n    },\n    \"confirmDeleteServer\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Are you sure you want to delete this server?\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"このサーバーを削除してもよろしいですか？\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Bu sunucuyu silmek istediğinizden emin misiniz?\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"确定删除服务器吗？\"\n          }\n        }\n      }\n    },\n    \"consoleComment\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"The console will print\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"コンソールに表示されます\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Konsola şunları yazdırır\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"控制台将打印\"\n          }\n        }\n      }\n    },\n    \"continuousSupport\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Continuous support\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"継続的なサポート\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"sürekli destek\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"持续支持\"\n          }\n        }\n      }\n    },\n    \"Copy\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Copied!\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"コピーしました！\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Kopyalandı!\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"复制成功\"\n          }\n        }\n      }\n    },\n    \"Copy2\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Copy\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"コピー\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Kopyala\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"复制\"\n          }\n        }\n      }\n    },\n    \"copyAddressAndKey\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Copy Address and Key\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"アドレスとキーをコピー\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Adres ve Anahtarı Kopyala\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"复制地址和Key\"\n          }\n        }\n      }\n    },\n    \"CopyContent\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Copy the Content\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"内容をコピー\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"İçeriği Kopyala\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"复制内容\"\n          }\n        }\n      }\n    },\n    \"copyExample\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Copy Send Script Example\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"送信スクリプトの例をコピー\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Kopya Gönderme Komut Dosyası Örneği\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"复制发送脚本示例\"\n          }\n        }\n      }\n    },\n    \"copyParameter\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Specify clipboard content by passing through the 'copy' parameter\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"'copy'パラメータを介してクリップボードの内容を指定します\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"'copy' parametresini geçirerek pano içeriğini belirtin\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"下拉推送、锁屏界面左滑查看推送时，可以选择复制推送内容\\n携带copy参数时，将只复制copy参数的值\"\n          }\n        }\n      }\n    },\n    \"criticalAlert\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Critical Alert\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"緊急アラート\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"kritik uyarı\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"重要警告\"\n          }\n        }\n      }\n    },\n    \"criticalAlertNotice\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Critical Alert will ignore silent and Do Not Disturb modes. \\nIf volume isn't provided, it defaults to 5 (range: 0-10).\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"緊急アラートはサイレントモードとおやすみモードを無視します。  \\n音量が指定されていない場合、デフォルトは5です（範囲: 0-10）。 \"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Critical Alert will ignore silent and Do Not Disturb modes. \\nIf volume isn't provided, it defaults to 5 (range: 0-10).\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"重要警告会忽略静音设置和勿扰模式，无论设备是否静音，通知都会提示并播放声音。\\n音量参数 volume 不传默认值为5， 取值范围: 0-10。\"\n          }\n        }\n      }\n    },\n    \"CustomedNotificationContent\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Body Text\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"本文\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Gövde Metni\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"这里改成你自己的推送内容\"\n          }\n        }\n      }\n    },\n    \"CustomedNotificationTitle\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Title\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"タイトル\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Başlık\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"推送标题\"\n          }\n        }\n      }\n    },\n    \"customSounds\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Custom Sounds\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"カスタムサウンド \"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"özel sesler\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"自定义铃声\"\n          }\n        }\n      }\n    },\n    \"default\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Default\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"デフォルト\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Varsayılan\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"默认\"\n          }\n        }\n      }\n    },\n    \"defaultArchiveSettings\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Archive by Default\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"デフォルトでアーカイブ\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Varsayılan Olarak Arşivle\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"默认保存\"\n          }\n        }\n      }\n    },\n    \"defaultSounds\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Default Sounds\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"デフォルトサウンド \"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"varsayılan sesler\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"默认铃声\"\n          }\n        }\n      }\n    },\n    \"deletedSuccessfully\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Server has been deleted successfully.\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"サーバーが正常に削除されました。\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Sunucu başarıyla silindi.\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"删除成功！\"\n          }\n        }\n      }\n    },\n    \"deleteFailed\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"You must have at least one server configured.\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"少なくとも1つのサーバーを設定する必要があります。\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Yapılandırılmış en az bir sunucunuz olmalıdır.\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"不能删除，服务器至少需保留一个\"\n          }\n        }\n      }\n    },\n    \"deleteServer\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Delete Server\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"サーバーを削除\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Sunucuyu Sil\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"删除服务器\"\n          }\n        }\n      }\n    },\n    \"demo\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Demo\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"デモ\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Demo\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"演示\"\n          }\n        }\n      }\n    },\n    \"DeploymentDocuments\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Deploy a self-hosted Bark server\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"セルフホスト型Barkサーバーをデプロイ\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Sunucu tarafı dağıtım dökümanını görüntüleyin\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"查看服务端部署教程\"\n          }\n        }\n      }\n    },\n    \"deployUrl\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"https://bark.day.app/#/en-us/deploy\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"https://bark.day.app/#/en-us/deploy\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"https://bark.day.app/#/en-us/deploy\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"https://bark.day.app/#/deploy\"\n          }\n        }\n      }\n    },\n    \"documentation\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Documentation\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"ドキュメント\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Dokümantasyon\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"使用文档\"\n          }\n        }\n      }\n    },\n    \"docUrl\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"https://bark.day.app/#/en-us/?id=bark\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"https://bark.day.app/#/en-us/?id=bark\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"https://bark.day.app/#/en-us/?id=bark\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"https://bark.day.app/#/?id=bark\"\n          }\n        }\n      }\n    },\n    \"donate\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Donate\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"寄付\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"bağış yapmak\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"捐赠\"\n          }\n        }\n      }\n    },\n    \"donateOK\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"🤝\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"🤝\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"🤝\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"🤝\"\n          }\n        }\n      }\n    },\n    \"done\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Done\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"完了\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Bitti\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"完成\"\n          }\n        }\n      }\n    },\n    \"encryptionNotice\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Uses a custom key to encrypt and decrypt the push content when sending and receiving. In this way, the push content will not be obtained or leaked by Bark server and Apple APNs server during transmission.\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"送受信時にカスタムキーを使用してプッシュ内容を暗号化および復号化します。これにより、プッシュ内容がBarkサーバーやApple APNsサーバーによって取得または漏洩されることはありません。\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Gönderirken ve alırken push içeriğini şifrelemek ve şifresini çözmek için özel bir anahtar kullanır. Bu sayede push içeriği iletim sırasında Bark sunucusu ve Apple APNs sunucusu tarafından elde edilemez veya sızdırılamaz.\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"在发送和接收推送时，对推送内容进行加密和解密。这样，推送内容在传输过程中就不会被 Bark 服务器和苹果 APNs 服务器获取或泄露，从而保护你的隐私\"\n          }\n        }\n      }\n    },\n    \"encryptionSettings\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Encryption Settings\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"暗号化設定\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Şifreleme Ayarları\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"加密设置\"\n          }\n        }\n      }\n    },\n    \"encryptionUrl\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"https://bark.day.app/#/en-us/encryption\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"https://bark.day.app/#/en-us/encryption\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"https://bark.day.app/#/en-us/encryption\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"https://bark.day.app/#/encryption\"\n          }\n        }\n      }\n    },\n    \"enterIv\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Please enter %d-bytes Iv\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"%dバイトのIvを入力してください \"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Lütfen %d-bytes Iv girin\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"请输入%d位Iv\"\n          }\n        }\n      }\n    },\n    \"enterKey\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Please enter %d-bytes Key\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"%dバイトのKeyを入力してください\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Lütfen %d-bytes Anahtar girin\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"请输入%d位Key\"\n          }\n        }\n      }\n    },\n    \"export\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Export\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"エクスポート\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Dışa Aktar\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"导出\"\n          }\n        }\n      }\n    },\n    \"faq\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"FAQs\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"よくある質問\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"SSS\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"常见问题\"\n          }\n        }\n      }\n    },\n    \"faqUrl\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"https://bark.day.app/#/en-us/faq\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"https://bark.day.app/#/en-us/faq\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"https://bark.day.app/#/en-us/faq\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"https://bark.day.app/#/faq\"\n          }\n        }\n      }\n    },\n    \"gcmNotSupported\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"GCM mode does not support copying script example.\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"GCMモードはスクリプト例のコピーをサポートしていません。\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"GCM modu, script örneklerini kopyalamayı desteklemez.\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"GCM Mode 不支持复制脚本示例。\"\n          }\n        }\n      }\n    },\n    \"group\" : {\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Group\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"グループ\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"grup\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"分组\"\n          }\n        }\n      }\n    },\n    \"groupMessagesNotice\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Let iOS sort notifications by groups.\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"iOSに通知をグループごとに並べ替えさせます\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"iOS'un bildirimleri gruplara göre sıralamasına izin verin.\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"对消息进行分组，推送将按group分组显示在通知中心中。\\n也可在历史消息列表中选择查看不同的群组。\"\n          }\n        }\n      }\n    },\n    \"groupMuted\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Group \\\"%@\\\" muted!\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"グループ「%@」をミュートしました！\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"\\\"%@\\\" grubunun sesi kapatıldı!\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"分组 \\\"%@\\\" 已静音\"\n          }\n        }\n      }\n    },\n    \"historyMessage\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Message History\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"メッセージ履歴\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Mesaj Geçmişi\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"历史消息\"\n          }\n        }\n      }\n    },\n    \"image\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Image\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"画像\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"resim\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"图片\"\n          }\n        }\n      }\n    },\n    \"imageParameter\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"To show a picture in a push banner and message list\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"プッシュバナーやメッセージ一覧に画像を表示するには、画像のURLを渡す必要があります。\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Gönderim afişlerinde ve mesaj listelerinde görsel göstermek için görsel URL’si iletilmelidir.\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"在推送横幅与消息列表中显示图片，需传递图片url。\"\n          }\n        }\n      }\n    },\n    \"imagePushNotification\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Image Push Notification\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"画像プッシュ通知\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Görsel Bildirim Uyarısı\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"图片推送通知\"\n          }\n        }\n      }\n    },\n    \"import\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Import\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"インポート\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"İçe Aktar\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"导入\"\n          }\n        }\n      }\n    },\n    \"info\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Info\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"情報\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Bilgi\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"信息\"\n          }\n        }\n      }\n    },\n    \"interruptionLevel\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Time Sensitive Notifications\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"時間感応通知\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Zamana Duyarlı Bildirimler\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"时效性通知\"\n          }\n        }\n      }\n    },\n    \"interruptionLevelNotice\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Set importance and delivery timing of a notification. Available values:\\nactive (default): presents the notification immediately, lights up the screen, and can play a sound.\\ntimeSensitive: similar to active, but can break through system controls such as Notification Summary and Focus.\\npassive: adds the notification to the notification list without lighting up the screen or playing a sound.\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"通知の重要度と配信タイミングを設定します。利用可能な値:  \\nactive（デフォルト）: 通知を即時に表示し、画面を点灯させ、音を再生することができます。  \\ntimeSensitive: activeと似ていますが、通知サマリーやフォーカスなどのシステム制御を突破できます。  \\npassive: 通知を通知リストに追加しますが、画面を点灯させたり音を再生したりしません。\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Bir bildirimin önemini ve teslim zamanlamasını ayarlayın. Kullanılabilir değerler:\\nactive (varsayılan): bildirimi hemen sunar, ekranı aydınlatır ve bir ses çalabilir.\\ntimeSensitive: aktif'e benzer, ancak Bildirim Özeti ve Odak gibi sistem denetimlerini aşabilir.\\npassive: bildirimi ekranı aydınlatmadan veya bir ses çalmadan bildirim listesine ekler.\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"可对通知设置中断级别，不设置则默认为 active。\\n\\n可选参数值：\\nactive： 默认值，系统会立即亮屏显示通知。\\ntimeSensitive：时效性通知，可在专注状态下显示通知。\\npassive：仅将通知添加到通知列表，不会亮屏提醒。\"\n          }\n        }\n      }\n    },\n    \"InvalidServer\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Server address is invalid.\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"サーバーアドレスが無効です。\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Sunucu adresi geçersiz.\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"填写的服务器无效，请重试!\"\n          }\n        }\n      }\n    },\n    \"InvalidURL\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"URL is invalid.\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"URLが無効です。\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"URL geçersiz.\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"输入的URL好像不对劲!\"\n          }\n        }\n      }\n    },\n    \"items\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"messages\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"メッセージ\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"mesajlar\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"条消息\"\n          }\n        }\n      }\n    },\n    \"ivComment\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"IV can be randomly generated, but if it is random, it needs to be passed in the iv parameter.\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"IVはランダムに生成できますが、ランダムな場合はivパラメータで渡す必要があります。\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"IV rastgele oluşturulabilir, ancak rastgele ise iv parametresinde belirtilmesi gerekir.\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"IV可以是随机生成的，但如果是随机的就需要放在 iv 参数里传递。\"\n          }\n        }\n      }\n    },\n    \"keyComment\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Must be %d bit long\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"%dビット長でなければなりません\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"%d bit uzunluğunda olmalıdır\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"必须%d位\"\n          }\n        }\n      }\n    },\n    \"lastHour\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"The last hour\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"過去1時間\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Son bir saat\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"最近一小时\"\n          }\n        }\n      }\n    },\n    \"lastMonth\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"The last month\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"過去1ヶ月\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Son Ay\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"最近一个月\"\n          }\n        }\n      }\n    },\n    \"messageGroup\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Notification Grouping\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"通知グループ化\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Bildirim Gruplandırma\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"推送消息分组\"\n          }\n        }\n      }\n    },\n    \"mode\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Mode\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"モード\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Mod\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"模式\"\n          }\n        }\n      }\n    },\n    \"more\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"More\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"詳細\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Daha\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"更多\"\n          }\n        }\n      }\n    },\n    \"MoreActions\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"More Actions\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"その他のアクション\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Daha Fazla İşlem\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"更多操作\"\n          }\n        }\n      }\n    },\n    \"muteGroup1Hour\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Mute this group for 1 Hour\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"このグループを1時間ミュート\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"bu grubu 1 saatliğine sessize alın\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"1小时内不再提醒此分组\"\n          }\n        }\n      }\n    },\n    \"noPermission\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"No permission\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"権限がありません\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"İzin yok\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"没有权限\"\n          }\n        }\n      }\n    },\n    \"Notice1\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Use the buttons to copy and open sample URLs\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"ボタンを使用してサンプルURLをコピーおよび開く\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Örnek URL'leri kopyalamak ve açmak için düğmeleri kullanın\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"点击右上角按钮可以复制测试URL、预览推送效果\\nSafari有缓存，没收到推送时请刷新页面\"\n          }\n        }\n      }\n    },\n    \"Notice2\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Customize notification title and body text\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"通知のタイトルと本文をカスタマイズ\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Bildirim başlığını ve gövde metnini özelleştirme\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"推送标题的字号比推送内容粗一点\"\n          }\n        }\n      }\n    },\n    \"notificationIcon\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Icon\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"アイコン\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"İkon\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"自定义推送图标（需iOS15或以上）\"\n          }\n        }\n      }\n    },\n    \"notificationIconNotice\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Display a custom icon on a notification.\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"通知にカスタムアイコンを表示\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Bildirimde özel bir ikon görüntüleyin.\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"为推送设置自定义图标，设置的图标将替换默认Bark图标。图标会自动缓存在本机，相同的图标 URL 仅下载一次。\"\n          }\n        }\n      }\n    },\n    \"notificationSound\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Alert Sound\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"アラートサウンド\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Uyarı Sesi\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"推送铃声\"\n          }\n        }\n      }\n    },\n    \"oneTimeDonation\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"One-time donation\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"一度きりの寄付\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"tek seferlik bağış\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"一次性捐赠\"\n          }\n        }\n      }\n    },\n    \"opensslEncodingComment\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"openssl requires Hex encoding of manual keys and IVs, not ASCII encoding.\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"opensslでは、手動キーとIVをASCIIエンコードではなくHexエンコードする必要があります。\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"openssl, manuel anahtarların ve IV'lerin ASCII kodlamasını değil, Hex kodlamasını gerektirir.\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"OpenSSL 要求输入的 Key 和 IV 需使用十六进制编码。\"\n          }\n        }\n      }\n    },\n    \"other\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Other\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"その他\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Diğer\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"其他\"\n          }\n        }\n      }\n    },\n    \"privacyPolicy\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Privacy Policy\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"プライバシーポリシー\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"gizlilik politikası\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"隐私政策\"\n          }\n        }\n      }\n    },\n    \"pushNotificationEncryption\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Push Notification Encryption\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"プッシュ通知暗号化\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Anlık Bildirim Şifreleme\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"推送加密\"\n          }\n        }\n      }\n    },\n    \"RegisterDevice\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Register Device\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"デバイスを登録\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Cihazı Kaydet\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"注册设备\"\n          }\n        }\n      }\n    },\n    \"removeMessage\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Remove\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"削除\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Sil\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"删除\"\n          }\n        }\n      }\n    },\n    \"removeNotice\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"This will be permanently removed and cannot be undone.\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"これは永久に削除され、元に戻せません。\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Bu kalıcı olarak kaldırılacak ve geri alınamaz.\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"将永久删除并且无法恢复。\"\n          }\n        }\n      }\n    },\n    \"resetFailed\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Reset Failed\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"リセット失敗\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Sıfırlama Başarısız Oldu\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"重置失败\"\n          }\n        }\n      }\n    },\n    \"resetFailed2\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Bark could not get a DeviceToken from the server.\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"BarkはサーバーからDeviceTokenを取得できませんでした。\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Bark sunucudan bir DeviceToken alamadı.\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"重置失败，没有获取到 DeviceToken\"\n          }\n        }\n      }\n    },\n    \"resetKey\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Reset or Restore Key\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"キーをリセットまたは復元\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Anahtarı Sıfırla veya Geri Yükle\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"重置或还原Key\"\n          }\n        }\n      }\n    },\n    \"resetKeyDesc\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Leave blank to reset. Enter an old key to restore.\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"空白のままにしてリセットします。古いキーを入力して復元します。\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Sıfırlamak için boş bırakın. Geri yüklemek için eski bir anahtar girin.\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"重置直接点确定，还原请先输入旧的Key\"\n          }\n        }\n      }\n    },\n    \"resetKeyPlaceholder\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Do not enter or enter the old Key\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"古いキーを入力しないか、空白のままにします\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Fazladan girmeyin veya T ve OL anahtarını girmeyin\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"不输入或输入旧的Key\"\n          }\n        }\n      }\n    },\n    \"restoreSubscription\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Restore Subscription\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"サブスクリプションを復元\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"aboneliği geri yükle\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"恢复订阅\"\n          }\n        }\n      }\n    },\n    \"ringtone\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Ringtone\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"着信音\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"zil sesi\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"持续响铃\"\n          }\n        }\n      }\n    },\n    \"ringtoneNotice\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Keep playing the ringtone for 30 seconds. With the level=critical parameter, it can ring continuously even in silent mode.\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"着信音を30秒間再生し続けます。level=criticalパラメータを使用すると、サイレントモードでも連続して鳴らすことができます。\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Zil sesi 30 saniye boyunca kesintisiz çalar. level=critical parametresi ile sessiz modda bile sürekli çalabilir.\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"持续播放铃声 30 秒，配合 level=critical 参数可在静音模式下持续响铃。\"\n          }\n        }\n      }\n    },\n    \"ringtoneVolume\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Ringtone Volume for Critical Alerts\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"重要な警告の着信音量\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Önemli Uyarılar için Zil Sesi Seviyesi\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"重要警告铃声音量\"\n          }\n        }\n      }\n    },\n    \"save\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Save\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"保存\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Kaydet\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"保存\"\n          }\n        }\n      }\n    },\n    \"saveSuccess\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Saved to your photos!\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"アルバムに保存したよ！\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Albüme kaydedildi!\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"已保存到相册！\"\n          }\n        }\n      }\n    },\n    \"sendPushNotification\" : {\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Send notification to this device\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"このデバイスにプッシュ通知を送信\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Bu cihaza bir bildirim gönder\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"发送推送到此设备\"\n          }\n        }\n      }\n    },\n    \"sendPushNotificationToOther\" : {\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Send notification to other device\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"他のデバイスにプッシュ通知を送信する\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Diğer cihazlara bildirim gönder\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"发送推送到其他设备\"\n          }\n        }\n      }\n    },\n    \"ServerAddress\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Server Address\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"サーバーアドレス\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Sunucu Adresi\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"服务器地址\"\n          }\n        }\n      }\n    },\n    \"ServerError\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Server Error\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"サーバーエラー\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Sunucu Hatası\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"服务器错误\"\n          }\n        }\n      }\n    },\n    \"ServerExample\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Enter the server address, e.g., https://api.day.app\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"サーバーアドレスを入力してください。例: https://api.day.app\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Sunucu adresini girin, örneğin, https://api.day.app\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"输入服务器地址，例如: https://api.day.app\"\n          }\n        }\n      }\n    },\n    \"serverList\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Server List\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"サーバーリスト\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Sunucu Listesi\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"服务器列表\"\n          }\n        }\n      }\n    },\n    \"service\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Service\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"サービス\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Hizmet\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"服务器\"\n          }\n        }\n      }\n    },\n    \"setAsDefaultServer\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Set As Default Server\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"デフォルトサーバーとして設定\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"varsayılan sunucu olarak ayarla\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"设为预览服务器\"\n          }\n        }\n      }\n    },\n    \"setServerName\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Set Server Name\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"サーバー名を設定\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Sunucu Adını Ayarla\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"设置服务器名称\"\n          }\n        }\n      }\n    },\n    \"setSounds\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Select an alert sound for a notification.\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"通知のアラートサウンドを選択\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Bildirim için bir uyarı sesi seçin.\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"可以为推送设置不同的铃声\"\n          }\n        }\n      }\n    },\n    \"setSuccessfully\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Successfully set\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"正常に設定されました\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"başarıyla ayarlandı\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"设置成功！\"\n          }\n        }\n      }\n    },\n    \"settings\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Settings\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"設定\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Ayarlar\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"设置\"\n          }\n        }\n      }\n    },\n    \"showLess\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Show less\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"詳細を表示しない\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Katlamak\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"折叠\"\n          }\n        }\n      }\n    },\n    \"sourceCode\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Source Code\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"ソースコード\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"kaynak kodu\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"开源代码\"\n          }\n        }\n      }\n    },\n    \"successfulDonation\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Donation\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"寄付\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"bağış\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"成功捐赠\"\n          }\n        }\n      }\n    },\n    \"thankYouSupport\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Thank you for your kind support\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"ご支援ありがとうございます\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"nazik desteğiniz için teşekkür ederim\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"谢谢你的慷慨支持\"\n          }\n        }\n      }\n    },\n    \"timeHourAgo\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"%1$d hour ago\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"%1$d時間前\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"%1$d saat önce\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"%1$d小时前\"\n          }\n        }\n      }\n    },\n    \"timeJustNow\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Just now\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"たった今\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Az önce\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"刚刚\"\n          }\n        }\n      }\n    },\n    \"timeMinAgo\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"%1$d minute ago\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"%1$d分前\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"%1$d dakika önce\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"%1$d分钟前\"\n          }\n        }\n      }\n    },\n    \"timeMinHourAgo\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"%1$d hour %2$d minutes ago\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"%1$d時間%2$d分前\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"%1$d saat %2$d dakika önce\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"%1$d小时%2$d分钟前\"\n          }\n        }\n      }\n    },\n    \"today\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Today\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"今日\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Bugün\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"今天\"\n          }\n        }\n      }\n    },\n    \"todayAndYesterday\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Today and yesterday\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"今日と昨日\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Dün ve Bugün\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"昨天和今天\"\n          }\n        }\n      }\n    },\n    \"toggle\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Toggle\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"表示切替\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Görünümü Değiştir\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"切换\"\n          }\n        }\n      }\n    },\n    \"unknown\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Unknown Status\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"不明なステータス\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Bilinmeyen Durum\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"未知\"\n          }\n        }\n      }\n    },\n    \"uploadSound\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Upload Sound\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"サウンドをアップロード\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"ses yükle\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"上传铃声\"\n          }\n        }\n      }\n    },\n    \"uploadSoundNoticeFullText\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Please convert the sound file to caf format, and make sure it's no longer than 30 seconds.\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"サウンドファイルをcaf形式に変換し、30秒以内であることを確認してください。\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"lütfen ses dosyasını TOC AF formatına dönüştürün ve SU GYO'ları 30 saniyeden uzun olmayacak şekilde işaretleyin.\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"请先将铃声转换成 caf 格式，时长不超过 30 秒。\"\n          }\n        }\n      }\n    },\n    \"uploadSoundNoticeHighlightText\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"convert the sound file to caf format\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"サウンドファイルをcaf形式に変換\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"ses dosyasını TOC AF formatına dönüştürün\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"转换成 caf 格式\"\n          }\n        }\n      }\n    },\n    \"urlParameter\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Open an URL by clicking on a notification, URL schemes are supported.\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"通知をクリックしてURLを開きます。URLスキームがサポートされています。\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Bir bildirime tıklayarak bir URL açın, URL şemaları desteklenir.\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"携带url参数时，点击推送会跳转到这个URL,支持跳转URL Scheme\"\n          }\n        }\n      }\n    },\n    \"userAgreement\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"User Agreement\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"利用規約\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"kullanıcı sözleşmesi\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"用户协议\"\n          }\n        }\n      }\n    },\n    \"version\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Version\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"バージョン\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Sürüm\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"版本\"\n          }\n        }\n      }\n    },\n    \"viewAllMessages\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"View all %d messages\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"すべての%d件のメッセージを表示\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Tüm %d mesajı görüntüle\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"查看全部%d条消息\"\n          }\n        }\n      }\n    },\n    \"viewAllSounds\" : {\n      \"extractionState\" : \"manual\",\n      \"localizations\" : {\n        \"en\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Click here to view all available sounds.\"\n          }\n        },\n        \"ja\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"ここをクリックして利用可能なすべてのサウンドを表示します。\"\n          }\n        },\n        \"tr\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"Mevcut tüm sesleri görüntülemek için buraya tıklayın.\"\n          }\n        },\n        \"zh-Hans\" : {\n          \"stringUnit\" : {\n            \"state\" : \"translated\",\n            \"value\" : \"查看所有铃声\"\n          }\n        }\n      }\n    }\n  },\n  \"version\" : \"1.0\"\n}"
  },
  {
    "path": "Bark.xcodeproj/project.pbxproj",
    "content": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 54;\n\tobjects = {\n\n/* Begin PBXBuildFile section */\n\t\t0603706720E1E31600F4CA05 /* Defines.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0603706620E1E31600F4CA05 /* Defines.swift */; };\n\t\t0603706920E1F89500F4CA05 /* PreviewCardCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0603706820E1F89500F4CA05 /* PreviewCardCell.swift */; };\n\t\t0603706B20E20A7C00F4CA05 /* String+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0603706A20E20A7C00F4CA05 /* String+Extension.swift */; };\n\t\t0603706D20E23EC000F4CA05 /* BarkSFSafariViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0603706C20E23EC000F4CA05 /* BarkSFSafariViewController.swift */; };\n\t\t060481EE250F404500BC9799 /* SoundsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 060481ED250F404500BC9799 /* SoundsViewController.swift */; };\n\t\t060481F0250F51CA00BC9799 /* SoundCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 060481EF250F51CA00BC9799 /* SoundCell.swift */; };\n\t\t0604F7DF20620D4900B32F09 /* ServerManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0604F7DE20620D4900B32F09 /* ServerManager.swift */; };\n\t\t0608F06E2994D115006B8029 /* BKDropDownCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0608F06D2994D115006B8029 /* BKDropDownCell.swift */; };\n\t\t0608F0722994D269006B8029 /* BKDropDownCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0608F0712994D269006B8029 /* BKDropDownCell.xib */; };\n\t\t06172FDA27F6DAEF002333A4 /* ServerListTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06172FD927F6DAEF002333A4 /* ServerListTableViewCell.swift */; };\n\t\t06172FDC27F6DB06002333A4 /* ServerListTableViewCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06172FDB27F6DB06002333A4 /* ServerListTableViewCellViewModel.swift */; };\n\t\t061894C529962EB900E001C2 /* GradientButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 061894C429962EB900E001C2 /* GradientButton.swift */; };\n\t\t061894C729A75BEA00E001C2 /* Algorithm.swift in Sources */ = {isa = PBXBuildFile; fileRef = 061894C629A75BEA00E001C2 /* Algorithm.swift */; };\n\t\t061C17082D1BDA4B00891D66 /* MessageGroupMoreView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 061C17072D1BDA4B00891D66 /* MessageGroupMoreView.swift */; };\n\t\t061E35862D1E5028009A2D6F /* MessageItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 061E35852D1E5028009A2D6F /* MessageItemModel.swift */; };\n\t\t0627DABB298B6EA2002F3F69 /* DropBoxView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0627DABA298B6EA2002F3F69 /* DropBoxView.swift */; };\n\t\t0627DABD2990D615002F3F69 /* BorderTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0627DABC2990D615002F3F69 /* BorderTextField.swift */; };\n\t\t062B98C3251B2762004562E7 /* BKButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062B98C2251B2762004562E7 /* BKButton.swift */; };\n\t\t062B98C8251B27AE004562E7 /* UINavigationItem+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062B98C7251B27AE004562E7 /* UINavigationItem+Extension.swift */; };\n\t\t0632050F250B6DD4001561EC /* gotosleep.caf in Resources */ = {isa = PBXBuildFile; fileRef = 063204F0250B6DD1001561EC /* gotosleep.caf */; };\n\t\t06320510250B6DD4001561EC /* paymentsuccess.caf in Resources */ = {isa = PBXBuildFile; fileRef = 063204F1250B6DD1001561EC /* paymentsuccess.caf */; };\n\t\t06320511250B6DD4001561EC /* shake.caf in Resources */ = {isa = PBXBuildFile; fileRef = 063204F2250B6DD1001561EC /* shake.caf */; };\n\t\t06320512250B6DD4001561EC /* alarm.caf in Resources */ = {isa = PBXBuildFile; fileRef = 063204F3250B6DD1001561EC /* alarm.caf */; };\n\t\t06320513250B6DD4001561EC /* bloom.caf in Resources */ = {isa = PBXBuildFile; fileRef = 063204F4250B6DD1001561EC /* bloom.caf */; };\n\t\t06320514250B6DD4001561EC /* sherwoodforest.caf in Resources */ = {isa = PBXBuildFile; fileRef = 063204F5250B6DD2001561EC /* sherwoodforest.caf */; };\n\t\t06320515250B6DD4001561EC /* healthnotification.caf in Resources */ = {isa = PBXBuildFile; fileRef = 063204F6250B6DD2001561EC /* healthnotification.caf */; };\n\t\t06320516250B6DD4001561EC /* calypso.caf in Resources */ = {isa = PBXBuildFile; fileRef = 063204F7250B6DD2001561EC /* calypso.caf */; };\n\t\t06320517250B6DD4001561EC /* descent.caf in Resources */ = {isa = PBXBuildFile; fileRef = 063204F8250B6DD2001561EC /* descent.caf */; };\n\t\t06320518250B6DD4001561EC /* ladder.caf in Resources */ = {isa = PBXBuildFile; fileRef = 063204F9250B6DD2001561EC /* ladder.caf */; };\n\t\t06320519250B6DD4001561EC /* tiptoes.caf in Resources */ = {isa = PBXBuildFile; fileRef = 063204FA250B6DD2001561EC /* tiptoes.caf */; };\n\t\t0632051A250B6DD4001561EC /* fanfare.caf in Resources */ = {isa = PBXBuildFile; fileRef = 063204FB250B6DD2001561EC /* fanfare.caf */; };\n\t\t0632051B250B6DD4001561EC /* birdsong.caf in Resources */ = {isa = PBXBuildFile; fileRef = 063204FC250B6DD2001561EC /* birdsong.caf */; };\n\t\t0632051C250B6DD4001561EC /* typewriters.caf in Resources */ = {isa = PBXBuildFile; fileRef = 063204FD250B6DD2001561EC /* typewriters.caf */; };\n\t\t0632051D250B6DD4001561EC /* anticipate.caf in Resources */ = {isa = PBXBuildFile; fileRef = 063204FE250B6DD2001561EC /* anticipate.caf */; };\n\t\t0632051E250B6DD4001561EC /* choo.caf in Resources */ = {isa = PBXBuildFile; fileRef = 063204FF250B6DD3001561EC /* choo.caf */; };\n\t\t0632051F250B6DD4001561EC /* glass.caf in Resources */ = {isa = PBXBuildFile; fileRef = 06320500250B6DD3001561EC /* glass.caf */; };\n\t\t06320520250B6DD4001561EC /* telegraph.caf in Resources */ = {isa = PBXBuildFile; fileRef = 06320501250B6DD3001561EC /* telegraph.caf */; };\n\t\t06320521250B6DD4001561EC /* multiwayinvitation.caf in Resources */ = {isa = PBXBuildFile; fileRef = 06320502250B6DD3001561EC /* multiwayinvitation.caf */; };\n\t\t06320522250B6DD4001561EC /* newmail.caf in Resources */ = {isa = PBXBuildFile; fileRef = 06320503250B6DD3001561EC /* newmail.caf */; };\n\t\t06320523250B6DD4001561EC /* update.caf in Resources */ = {isa = PBXBuildFile; fileRef = 06320504250B6DD3001561EC /* update.caf */; };\n\t\t06320524250B6DD4001561EC /* minuet.caf in Resources */ = {isa = PBXBuildFile; fileRef = 06320505250B6DD3001561EC /* minuet.caf */; };\n\t\t06320525250B6DD4001561EC /* suspense.caf in Resources */ = {isa = PBXBuildFile; fileRef = 06320506250B6DD3001561EC /* suspense.caf */; };\n\t\t06320526250B6DD4001561EC /* mailsent.caf in Resources */ = {isa = PBXBuildFile; fileRef = 06320507250B6DD3001561EC /* mailsent.caf */; };\n\t\t06320527250B6DD4001561EC /* noir.caf in Resources */ = {isa = PBXBuildFile; fileRef = 06320508250B6DD3001561EC /* noir.caf */; };\n\t\t06320528250B6DD4001561EC /* chime.caf in Resources */ = {isa = PBXBuildFile; fileRef = 06320509250B6DD4001561EC /* chime.caf */; };\n\t\t06320529250B6DD4001561EC /* spell.caf in Resources */ = {isa = PBXBuildFile; fileRef = 0632050A250B6DD4001561EC /* spell.caf */; };\n\t\t0632052A250B6DD4001561EC /* electronic.caf in Resources */ = {isa = PBXBuildFile; fileRef = 0632050B250B6DD4001561EC /* electronic.caf */; };\n\t\t0632052B250B6DD4001561EC /* bell.caf in Resources */ = {isa = PBXBuildFile; fileRef = 0632050C250B6DD4001561EC /* bell.caf */; };\n\t\t0632052C250B6DD4001561EC /* horn.caf in Resources */ = {isa = PBXBuildFile; fileRef = 0632050D250B6DD4001561EC /* horn.caf */; };\n\t\t0632052D250B6DD4001561EC /* newsflash.caf in Resources */ = {isa = PBXBuildFile; fileRef = 0632050E250B6DD4001561EC /* newsflash.caf */; };\n\t\t0632CE1F20EC9098003FDF46 /* UserNotifications.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0683487020510FB20024B6DA /* UserNotifications.framework */; };\n\t\t0632CE2020EC9098003FDF46 /* UserNotificationsUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0683487220510FB20024B6DA /* UserNotificationsUI.framework */; };\n\t\t0632CE2320EC9098003FDF46 /* NotificationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0632CE2220EC9098003FDF46 /* NotificationViewController.swift */; };\n\t\t0632CE2620EC9098003FDF46 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0632CE2420EC9098003FDF46 /* MainInterface.storyboard */; };\n\t\t0632CE2A20EC9098003FDF46 /* NotificationContentExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 0632CE1E20EC9098003FDF46 /* NotificationContentExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };\n\t\t0633E80A256A091B00ED0680 /* MJRefresh+Rx.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0633E809256A091B00ED0680 /* MJRefresh+Rx.swift */; };\n\t\t0635A8052CE47DFE0027E00F /* SettingSectionHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0635A8042CE47DFE0027E00F /* SettingSectionHeader.swift */; };\n\t\t0635A8072CE4883A0027E00F /* DonateCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0635A8062CE4883A0027E00F /* DonateCell.swift */; };\n\t\t0637FA7820E0926D00E80174 /* BarkTargetType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0637FA7720E0926D00E80174 /* BarkTargetType.swift */; };\n\t\t0637FA7A20E092B300E80174 /* Observable+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0637FA7920E092B300E80174 /* Observable+Extension.swift */; };\n\t\t0637FA7C20E0930E00E80174 /* BarkApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0637FA7B20E0930E00E80174 /* BarkApi.swift */; };\n\t\t0637FA7E20E0969800E80174 /* Client.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0637FA7D20E0969800E80174 /* Client.swift */; };\n\t\t0637FA8020E0981E00E80174 /* BarkSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0637FA7F20E0981E00E80174 /* BarkSettings.swift */; };\n\t\t0637FA8220E09C4B00E80174 /* BarkNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0637FA8120E09C4B00E80174 /* BarkNavigationController.swift */; };\n\t\t0637FA8620E0AB6600E80174 /* UIColor+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0637FA8520E0AB6600E80174 /* UIColor+Extension.swift */; };\n\t\t0637FA8A20E0D58800E80174 /* NewServerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0637FA8920E0D58800E80174 /* NewServerViewController.swift */; };\n\t\t0637FA8C20E0D7A700E80174 /* BaseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0637FA8B20E0D7A700E80174 /* BaseViewController.swift */; };\n\t\t063B909B272149BF00431EC2 /* HomeViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 063B909A272149BF00431EC2 /* HomeViewModelTests.swift */; };\n\t\t0642B55A27EB13F100453D91 /* MutableTextCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0642B55927EB13F100453D91 /* MutableTextCell.swift */; };\n\t\t0642B55C27EB149900453D91 /* MutableTextCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0642B55B27EB149900453D91 /* MutableTextCellViewModel.swift */; };\n\t\t0647DB682CE604AA00102066 /* MessageSettingFooter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0647DB672CE604AA00102066 /* MessageSettingFooter.swift */; };\n\t\t064CAB9E256BE9090018155C /* PreviewCardCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 064CAB9D256BE9090018155C /* PreviewCardCellViewModel.swift */; };\n\t\t064CABA6256BE9510018155C /* PreviewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 064CABA5256BE9510018155C /* PreviewModel.swift */; };\n\t\t0653677629B719BC0038BDB8 /* CryptoSettingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06F08EA329B098DD006AB9CA /* CryptoSettingManager.swift */; };\n\t\t0653677829B727A60038BDB8 /* CryptoSettingRelay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0653677729B727A60038BDB8 /* CryptoSettingRelay.swift */; };\n\t\t065752C12CB6844A001426D8 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 065752C02CB6844A001426D8 /* Localizable.xcstrings */; };\n\t\t065752C22CB6844A001426D8 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 065752C02CB6844A001426D8 /* Localizable.xcstrings */; };\n\t\t065752C32CB6844A001426D8 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 065752C02CB6844A001426D8 /* Localizable.xcstrings */; };\n\t\t065AE76B2987777F00323230 /* ArchiveSettingRelay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 065AE76A2987777F00323230 /* ArchiveSettingRelay.swift */; };\n\t\t065BE4402563D649002A8CA4 /* SoundsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 065BE43F2563D649002A8CA4 /* SoundsViewModel.swift */; };\n\t\t065BE4462563D7E5002A8CA4 /* ViewModelType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 065BE4452563D7E5002A8CA4 /* ViewModelType.swift */; };\n\t\t065BE44B2563D8E1002A8CA4 /* Reusable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 065BE44A2563D8E1002A8CA4 /* Reusable.swift */; };\n\t\t065BE4502563D939002A8CA4 /* SoundCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 065BE44F2563D939002A8CA4 /* SoundCellViewModel.swift */; };\n\t\t065BE4552565055F002A8CA4 /* HomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 065BE4542565055F002A8CA4 /* HomeViewModel.swift */; };\n\t\t0661A543204FDA4100965E4E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0661A542204FDA4100965E4E /* AppDelegate.swift */; };\n\t\t0661A545204FDA4100965E4E /* HomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0661A544204FDA4100965E4E /* HomeViewController.swift */; };\n\t\t0661A54A204FDA4100965E4E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0661A549204FDA4100965E4E /* Assets.xcassets */; };\n\t\t0661A54D204FDA4100965E4E /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0661A54B204FDA4100965E4E /* LaunchScreen.storyboard */; };\n\t\t0667D192247D162C005DE2ED /* MessageTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0667D191247D162C005DE2ED /* MessageTableViewCell.swift */; };\n\t\t0667D194247D1BA0005DE2ED /* Date+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0667D193247D1BA0005DE2ED /* Date+Extension.swift */; };\n\t\t066890082D1946D500E106F2 /* MessageItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 066890072D1946D500E106F2 /* MessageItemView.swift */; };\n\t\t0668900B2D19525400E106F2 /* ShowLessAndClearView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0668900A2D19525400E106F2 /* ShowLessAndClearView.swift */; };\n\t\t0668900D2D19582400E106F2 /* MessageGroupHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0668900C2D19582400E106F2 /* MessageGroupHeaderView.swift */; };\n\t\t066DF4822D2D31A60092B04E /* MessageDeleteTimeRange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 066DF4812D2D31A30092B04E /* MessageDeleteTimeRange.swift */; };\n\t\t066E0C8C2BB6AC9A00873838 /* AddSoundCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 066E0C8B2BB6AC9A00873838 /* AddSoundCell.swift */; };\n\t\t0672CB06256903F700570C9D /* MessageListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0672CB05256903F700570C9D /* MessageListViewModel.swift */; };\n\t\t06787C392A710568008ABDD7 /* GesturePassTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06787C382A710568008ABDD7 /* GesturePassTextView.swift */; };\n\t\t067AFB1C2E5D8BE300AE78E7 /* UNNotificationContent+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 067AFB1B2E5D8BE300AE78E7 /* UNNotificationContent+Extension.swift */; };\n\t\t067AFB1D2E5D8BED00AE78E7 /* UNNotificationContent+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 067AFB1B2E5D8BE300AE78E7 /* UNNotificationContent+Extension.swift */; };\n\t\t067B2EB525693E38008B6BE1 /* MessageSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 067B2EB425693E38008B6BE1 /* MessageSection.swift */; };\n\t\t06802E5320ECC40C00767047 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0661A549204FDA4100965E4E /* Assets.xcassets */; };\n\t\t06840DBB272298FB001B3193 /* BKColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06840DBA272298FB001B3193 /* BKColor.swift */; };\n\t\t0687F2A82CCB791A00B2A52F /* UIFont+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0687F2A72CCB791A00B2A52F /* UIFont+Extension.swift */; };\n\t\t0687F2A92CCB791A00B2A52F /* UIFont+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0687F2A72CCB791A00B2A52F /* UIFont+Extension.swift */; };\n\t\t0687F2AA2CCB7FA500B2A52F /* UIFont+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0687F2A72CCB791A00B2A52F /* UIFont+Extension.swift */; };\n\t\t06885EB6247FB9880004A303 /* MessageSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06885EB5247FB9880004A303 /* MessageSettingsViewController.swift */; };\n\t\t0689CF4C2C7484A7007203A6 /* BarkTabBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0689CF4B2C7484A7007203A6 /* BarkTabBarController.swift */; };\n\t\t068A4B962D2E11CD00982449 /* MessageDeleteTimeRangeTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 068A4B952D2E11CD00982449 /* MessageDeleteTimeRangeTest.swift */; };\n\t\t068EC15827ED99C900D5D11E /* ServerListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 068EC15727ED99C900D5D11E /* ServerListViewController.swift */; };\n\t\t068EC15A27ED99E700D5D11E /* ServerListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 068EC15927ED99E700D5D11E /* ServerListViewModel.swift */; };\n\t\t068F66B3247BD84C00DAD25A /* MessageListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 068F66B2247BD84C00DAD25A /* MessageListViewController.swift */; };\n\t\t069332222E6A8E3100F9387F /* String+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0603706A20E20A7C00F4CA05 /* String+Extension.swift */; };\n\t\t069332232E6A8E3100F9387F /* String+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0603706A20E20A7C00F4CA05 /* String+Extension.swift */; };\n\t\t0699473D2D223094008D5E40 /* CustomTapTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0699473C2D223094008D5E40 /* CustomTapTextView.swift */; };\n\t\t069C6CA42ED03C72007244BB /* MarkdownProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 069C6CA32ED03C72007244BB /* MarkdownProcessor.swift */; };\n\t\t069C6CA52ED03E10007244BB /* MarkdownParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06C777CF2ECEFF210032A044 /* MarkdownParser.swift */; };\n\t\t069C6CA72ED03E27007244BB /* Markdown in Frameworks */ = {isa = PBXBuildFile; productRef = 069C6CA62ED03E27007244BB /* Markdown */; };\n\t\t069C6CA92ED03F07007244BB /* BKColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06840DBA272298FB001B3193 /* BKColor.swift */; };\n\t\t06B1158D247BA6D5006D91FB /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 06B1158C247BA6D5006D91FB /* CloudKit.framework */; };\n\t\t06B1158F247BB1FB006D91FB /* Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06B1158E247BB1FB006D91FB /* Message.swift */; };\n\t\t06B11591247BC132006D91FB /* Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06B1158E247BB1FB006D91FB /* Message.swift */; };\n\t\t06BBB88725650C6C0076F63E /* ArchiveSettingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06C5953224811505006B98F3 /* ArchiveSettingManager.swift */; };\n\t\t06BBB89125650CCF0076F63E /* ArchiveSettingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06C5953224811505006B98F3 /* ArchiveSettingManager.swift */; };\n\t\t06BBB896256518760076F63E /* NewServerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06BBB895256518760076F63E /* NewServerViewModel.swift */; };\n\t\t06BBB8B72567AC140076F63E /* MessageSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06BBB8B62567AC140076F63E /* MessageSettingsViewModel.swift */; };\n\t\t06BBB8BC2567B3AD0076F63E /* ArchiveSettingCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06BBB8BB2567B3AD0076F63E /* ArchiveSettingCellViewModel.swift */; };\n\t\t06BBB8C12567B3EF0076F63E /* BaseTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06BBB8C02567B3EF0076F63E /* BaseTableViewCell.swift */; };\n\t\t06BBB8C92567B6730076F63E /* Operators.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06BBB8C82567B6730076F63E /* Operators.swift */; };\n\t\t06BBB8CE2567B8E60076F63E /* silence.caf in Resources */ = {isa = PBXBuildFile; fileRef = 06BBB8CD2567B8E60076F63E /* silence.caf */; };\n\t\t06BCAE562CDB19260092867A /* GroupMuteSettingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06BCAE552CDB19260092867A /* GroupMuteSettingManager.swift */; };\n\t\t06BCAE572CDB19420092867A /* GroupMuteSettingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06BCAE552CDB19260092867A /* GroupMuteSettingManager.swift */; };\n\t\t06BCAE582CDB19420092867A /* GroupMuteSettingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06BCAE552CDB19260092867A /* GroupMuteSettingManager.swift */; };\n\t\t06BCAE5B2CDB25120092867A /* MuteProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06BCAE5A2CDB25120092867A /* MuteProcessor.swift */; };\n\t\t06BD4DAA2901352E003364DB /* Object+Dictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06BD4DA92901352E003364DB /* Object+Dictionary.swift */; };\n\t\t06BE84042E6EAE7100E6F856 /* InsetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06BE84032E6EAE7100E6F856 /* InsetView.swift */; };\n\t\t06C2CF232685B88D0034B127 /* TextCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06C2CF222685B88D0034B127 /* TextCell.swift */; };\n\t\t06C2CF252685BDB80034B127 /* SpacerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06C2CF242685BDB80034B127 /* SpacerCell.swift */; };\n\t\t06C5952D2480E3F8006B98F3 /* LabelCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06C5952C2480E3F8006B98F3 /* LabelCell.swift */; };\n\t\t06C5953124811392006B98F3 /* ArchiveSettingCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06C5953024811392006B98F3 /* ArchiveSettingCell.swift */; };\n\t\t06C595362481160F006B98F3 /* BKLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06C595352481160F006B98F3 /* BKLabel.swift */; };\n\t\t06C777D02ECEFF210032A044 /* MarkdownParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06C777CF2ECEFF210032A044 /* MarkdownParser.swift */; };\n\t\t06C777D32ECF05990032A044 /* Markdown in Frameworks */ = {isa = PBXBuildFile; productRef = 06C777D22ECF05990032A044 /* Markdown */; };\n\t\t06CF784721C7A50300A052D7 /* NotificationServiceExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 06CF784021C7A50300A052D7 /* NotificationServiceExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };\n\t\t06CF784C21C7A51200A052D7 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06CF784B21C7A51200A052D7 /* NotificationService.swift */; };\n\t\t06D69E202C1159E200161A35 /* glass.caf in Resources */ = {isa = PBXBuildFile; fileRef = 06320500250B6DD3001561EC /* glass.caf */; };\n\t\t06D69E212C1159E200161A35 /* sherwoodforest.caf in Resources */ = {isa = PBXBuildFile; fileRef = 063204F5250B6DD2001561EC /* sherwoodforest.caf */; };\n\t\t06D69E222C1159E200161A35 /* ladder.caf in Resources */ = {isa = PBXBuildFile; fileRef = 063204F9250B6DD2001561EC /* ladder.caf */; };\n\t\t06D69E232C1159E200161A35 /* chime.caf in Resources */ = {isa = PBXBuildFile; fileRef = 06320509250B6DD4001561EC /* chime.caf */; };\n\t\t06D69E242C1159E200161A35 /* anticipate.caf in Resources */ = {isa = PBXBuildFile; fileRef = 063204FE250B6DD2001561EC /* anticipate.caf */; };\n\t\t06D69E252C1159E200161A35 /* update.caf in Resources */ = {isa = PBXBuildFile; fileRef = 06320504250B6DD3001561EC /* update.caf */; };\n\t\t06D69E262C1159E200161A35 /* suspense.caf in Resources */ = {isa = PBXBuildFile; fileRef = 06320506250B6DD3001561EC /* suspense.caf */; };\n\t\t06D69E272C1159E200161A35 /* newmail.caf in Resources */ = {isa = PBXBuildFile; fileRef = 06320503250B6DD3001561EC /* newmail.caf */; };\n\t\t06D69E282C1159E200161A35 /* noir.caf in Resources */ = {isa = PBXBuildFile; fileRef = 06320508250B6DD3001561EC /* noir.caf */; };\n\t\t06D69E292C1159E200161A35 /* birdsong.caf in Resources */ = {isa = PBXBuildFile; fileRef = 063204FC250B6DD2001561EC /* birdsong.caf */; };\n\t\t06D69E2A2C1159E200161A35 /* minuet.caf in Resources */ = {isa = PBXBuildFile; fileRef = 06320505250B6DD3001561EC /* minuet.caf */; };\n\t\t06D69E2B2C1159E200161A35 /* shake.caf in Resources */ = {isa = PBXBuildFile; fileRef = 063204F2250B6DD1001561EC /* shake.caf */; };\n\t\t06D69E2C2C1159E200161A35 /* newsflash.caf in Resources */ = {isa = PBXBuildFile; fileRef = 0632050E250B6DD4001561EC /* newsflash.caf */; };\n\t\t06D69E2D2C1159E200161A35 /* paymentsuccess.caf in Resources */ = {isa = PBXBuildFile; fileRef = 063204F1250B6DD1001561EC /* paymentsuccess.caf */; };\n\t\t06D69E2E2C1159E200161A35 /* descent.caf in Resources */ = {isa = PBXBuildFile; fileRef = 063204F8250B6DD2001561EC /* descent.caf */; };\n\t\t06D69E2F2C1159E200161A35 /* mailsent.caf in Resources */ = {isa = PBXBuildFile; fileRef = 06320507250B6DD3001561EC /* mailsent.caf */; };\n\t\t06D69E302C1159E200161A35 /* tiptoes.caf in Resources */ = {isa = PBXBuildFile; fileRef = 063204FA250B6DD2001561EC /* tiptoes.caf */; };\n\t\t06D69E312C1159E200161A35 /* telegraph.caf in Resources */ = {isa = PBXBuildFile; fileRef = 06320501250B6DD3001561EC /* telegraph.caf */; };\n\t\t06D69E322C1159E200161A35 /* healthnotification.caf in Resources */ = {isa = PBXBuildFile; fileRef = 063204F6250B6DD2001561EC /* healthnotification.caf */; };\n\t\t06D69E332C1159E200161A35 /* typewriters.caf in Resources */ = {isa = PBXBuildFile; fileRef = 063204FD250B6DD2001561EC /* typewriters.caf */; };\n\t\t06D69E342C1159E200161A35 /* bell.caf in Resources */ = {isa = PBXBuildFile; fileRef = 0632050C250B6DD4001561EC /* bell.caf */; };\n\t\t06D69E352C1159E200161A35 /* bloom.caf in Resources */ = {isa = PBXBuildFile; fileRef = 063204F4250B6DD1001561EC /* bloom.caf */; };\n\t\t06D69E362C1159E200161A35 /* spell.caf in Resources */ = {isa = PBXBuildFile; fileRef = 0632050A250B6DD4001561EC /* spell.caf */; };\n\t\t06D69E372C1159E200161A35 /* choo.caf in Resources */ = {isa = PBXBuildFile; fileRef = 063204FF250B6DD3001561EC /* choo.caf */; };\n\t\t06D69E382C1159E200161A35 /* multiwayinvitation.caf in Resources */ = {isa = PBXBuildFile; fileRef = 06320502250B6DD3001561EC /* multiwayinvitation.caf */; };\n\t\t06D69E392C1159E200161A35 /* horn.caf in Resources */ = {isa = PBXBuildFile; fileRef = 0632050D250B6DD4001561EC /* horn.caf */; };\n\t\t06D69E3A2C1159E200161A35 /* electronic.caf in Resources */ = {isa = PBXBuildFile; fileRef = 0632050B250B6DD4001561EC /* electronic.caf */; };\n\t\t06D69E3B2C1159E200161A35 /* calypso.caf in Resources */ = {isa = PBXBuildFile; fileRef = 063204F7250B6DD2001561EC /* calypso.caf */; };\n\t\t06D69E3C2C1159E200161A35 /* gotosleep.caf in Resources */ = {isa = PBXBuildFile; fileRef = 063204F0250B6DD1001561EC /* gotosleep.caf */; };\n\t\t06D69E3D2C1159E200161A35 /* silence.caf in Resources */ = {isa = PBXBuildFile; fileRef = 06BBB8CD2567B8E60076F63E /* silence.caf */; };\n\t\t06D69E3E2C1159E200161A35 /* alarm.caf in Resources */ = {isa = PBXBuildFile; fileRef = 063204F3250B6DD1001561EC /* alarm.caf */; };\n\t\t06D69E3F2C1159E200161A35 /* fanfare.caf in Resources */ = {isa = PBXBuildFile; fileRef = 063204FB250B6DD2001561EC /* fanfare.caf */; };\n\t\t06D69E412C11983E00161A35 /* CallProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06D69E402C11983E00161A35 /* CallProcessor.swift */; };\n\t\t06E62C112D670F62004DC82B /* PushToCurrentIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06E62C102D670F5D004DC82B /* PushToCurrentIntent.swift */; };\n\t\t06E944682C06E40600AC86AB /* NotificationContentProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06E944672C06E40600AC86AB /* NotificationContentProcessor.swift */; };\n\t\t06E9446A2C06E4A200AC86AB /* CiphertextProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06E944692C06E4A200AC86AB /* CiphertextProcessor.swift */; };\n\t\t06E9446D2C06FEC900AC86AB /* LevelProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06E9446C2C06FEC900AC86AB /* LevelProcessor.swift */; };\n\t\t06E9446F2C06FF1E00AC86AB /* BadgeProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06E9446E2C06FF1E00AC86AB /* BadgeProcessor.swift */; };\n\t\t06E944712C06FF4C00AC86AB /* AutoCopyProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06E944702C06FF4C00AC86AB /* AutoCopyProcessor.swift */; };\n\t\t06E944732C06FF9200AC86AB /* ArchiveProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06E944722C06FF9200AC86AB /* ArchiveProcessor.swift */; };\n\t\t06E944752C07012E00AC86AB /* RealmConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06E944742C07012E00AC86AB /* RealmConfiguration.swift */; };\n\t\t06E944762C07013000AC86AB /* RealmConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06E944742C07012E00AC86AB /* RealmConfiguration.swift */; };\n\t\t06E944782C0701F300AC86AB /* IconProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06E944772C0701F300AC86AB /* IconProcessor.swift */; };\n\t\t06E9447A2C0704E500AC86AB /* ImageDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06E944792C0704E500AC86AB /* ImageDownloader.swift */; };\n\t\t06E9447C2C07052F00AC86AB /* ImageProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06E9447B2C07052F00AC86AB /* ImageProcessor.swift */; };\n\t\t06EE1FD326843E9300586708 /* BarkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06EE1FD226843E9300586708 /* BarkTests.swift */; };\n\t\t06EEF333291CCFF400CA228A /* CryptoSettingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06EEF332291CCFF400CA228A /* CryptoSettingController.swift */; };\n\t\t06EEF335291CD00000CA228A /* CryptoSettingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06EEF334291CD00000CA228A /* CryptoSettingViewModel.swift */; };\n\t\t06EF49152D682A99008B91D2 /* PushResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06EF49142D682A96008B91D2 /* PushResponse.swift */; };\n\t\t06EF49172D682AC4008B91D2 /* OptionsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06EF49162D682AC3008B91D2 /* OptionsProvider.swift */; };\n\t\t06EF49192D682B3B008B91D2 /* PushToOtherIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06EF49182D682B34008B91D2 /* PushToOtherIntent.swift */; };\n\t\t06F08EA429B098DD006AB9CA /* CryptoSettingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06F08EA329B098DD006AB9CA /* CryptoSettingManager.swift */; };\n\t\t06F08EA529B1DDA7006AB9CA /* Algorithm.swift in Sources */ = {isa = PBXBuildFile; fileRef = 061894C629A75BEA00E001C2 /* Algorithm.swift */; };\n\t\t06F08EA729B1DDFE006AB9CA /* Error+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06F08EA629B1DDFE006AB9CA /* Error+Extension.swift */; };\n\t\t06F08EA829B1DE0A006AB9CA /* Error+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06F08EA629B1DDFE006AB9CA /* Error+Extension.swift */; };\n\t\t06F08EAF29B5D9FF006AB9CA /* HUD.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06F08EAE29B5D9FF006AB9CA /* HUD.swift */; };\n\t\t06F11E7727D9D5FB00F00298 /* QRScannerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06F11E7627D9D5FB00F00298 /* QRScannerViewController.swift */; };\n\t\t06F808912EF4056C0007352A /* AppDelegate+Realm.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06F808902EF4056C0007352A /* AppDelegate+Realm.swift */; };\n\t\t06FB04042C53575400F3A213 /* SharedDefines.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06FB04032C53575400F3A213 /* SharedDefines.swift */; };\n\t\t06FB04052C53575400F3A213 /* SharedDefines.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06FB04032C53575400F3A213 /* SharedDefines.swift */; };\n\t\t19BE8EBF2D03514A009BF080 /* ImageDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06E944792C0704E500AC86AB /* ImageDownloader.swift */; };\n\t\t1E73F99E2C282822002BF649 /* SectionViewController-iPad.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E73F99D2C282822002BF649 /* SectionViewController-iPad.swift */; };\n\t\t1EFB545D2C314A6800B8E51B /* BarkSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EFB545C2C314A6800B8E51B /* BarkSplitViewController.swift */; };\n\t\t1EFB545F2C32514000B8E51B /* SectionViewModel-iPad.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EFB545E2C32514000B8E51B /* SectionViewModel-iPad.swift */; };\n\t\t3428272069AFAFE2C683FEB0 /* libPods-Bark.a in Frameworks */ = {isa = PBXBuildFile; fileRef = CCC722470308049D180876C7 /* libPods-Bark.a */; };\n\t\t879AE4D4178855A9672009E4 /* libPods-NotificationServiceExtension.a in Frameworks */ = {isa = PBXBuildFile; fileRef = B7F8BDFAA047451561798F58 /* libPods-NotificationServiceExtension.a */; };\n\t\t88757E3ACE0EA6A48577C958 /* libPods-NotificationContentExtension.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6616B1ACAA7E422700D90C2B /* libPods-NotificationContentExtension.a */; };\n\t\tB963F7D5BA7AC2571E71EF66 /* libPods-BarkTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 76381A752CCCD4DA6BB2A566 /* libPods-BarkTests.a */; };\n/* End PBXBuildFile section */\n\n/* Begin PBXContainerItemProxy section */\n\t\t0632CE2820EC9098003FDF46 /* PBXContainerItemProxy */ = {\n\t\t\tisa = PBXContainerItemProxy;\n\t\t\tcontainerPortal = 0661A537204FDA4100965E4E /* Project object */;\n\t\t\tproxyType = 1;\n\t\t\tremoteGlobalIDString = 0632CE1D20EC9098003FDF46;\n\t\t\tremoteInfo = NotificationContentExtension;\n\t\t};\n\t\t06CF784521C7A50300A052D7 /* PBXContainerItemProxy */ = {\n\t\t\tisa = PBXContainerItemProxy;\n\t\t\tcontainerPortal = 0661A537204FDA4100965E4E /* Project object */;\n\t\t\tproxyType = 1;\n\t\t\tremoteGlobalIDString = 06CF783F21C7A50300A052D7;\n\t\t\tremoteInfo = NotificationServiceExtension;\n\t\t};\n\t\t06EE1FD526843E9300586708 /* PBXContainerItemProxy */ = {\n\t\t\tisa = PBXContainerItemProxy;\n\t\t\tcontainerPortal = 0661A537204FDA4100965E4E /* Project object */;\n\t\t\tproxyType = 1;\n\t\t\tremoteGlobalIDString = 0661A53E204FDA4100965E4E;\n\t\t\tremoteInfo = Bark;\n\t\t};\n/* End PBXContainerItemProxy section */\n\n/* Begin PBXCopyFilesBuildPhase section */\n\t\t0632CE2E20EC9098003FDF46 /* Embed Foundation Extensions */ = {\n\t\t\tisa = PBXCopyFilesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tdstPath = \"\";\n\t\t\tdstSubfolderSpec = 13;\n\t\t\tfiles = (\n\t\t\t\t0632CE2A20EC9098003FDF46 /* NotificationContentExtension.appex in Embed Foundation Extensions */,\n\t\t\t\t06CF784721C7A50300A052D7 /* NotificationServiceExtension.appex in Embed Foundation Extensions */,\n\t\t\t);\n\t\t\tname = \"Embed Foundation Extensions\";\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXCopyFilesBuildPhase section */\n\n/* Begin PBXFileReference section */\n\t\t0603706620E1E31600F4CA05 /* Defines.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Defines.swift; sourceTree = \"<group>\"; };\n\t\t0603706820E1F89500F4CA05 /* PreviewCardCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewCardCell.swift; sourceTree = \"<group>\"; };\n\t\t0603706A20E20A7C00F4CA05 /* String+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"String+Extension.swift\"; sourceTree = \"<group>\"; };\n\t\t0603706C20E23EC000F4CA05 /* BarkSFSafariViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BarkSFSafariViewController.swift; sourceTree = \"<group>\"; };\n\t\t060481ED250F404500BC9799 /* SoundsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoundsViewController.swift; sourceTree = \"<group>\"; };\n\t\t060481EF250F51CA00BC9799 /* SoundCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoundCell.swift; sourceTree = \"<group>\"; };\n\t\t0604F7DE20620D4900B32F09 /* ServerManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerManager.swift; sourceTree = \"<group>\"; };\n\t\t0608F06D2994D115006B8029 /* BKDropDownCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BKDropDownCell.swift; sourceTree = \"<group>\"; };\n\t\t0608F0712994D269006B8029 /* BKDropDownCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = BKDropDownCell.xib; sourceTree = \"<group>\"; };\n\t\t06172FD927F6DAEF002333A4 /* ServerListTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerListTableViewCell.swift; sourceTree = \"<group>\"; };\n\t\t06172FDB27F6DB06002333A4 /* ServerListTableViewCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerListTableViewCellViewModel.swift; sourceTree = \"<group>\"; };\n\t\t061894C429962EB900E001C2 /* GradientButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GradientButton.swift; sourceTree = \"<group>\"; };\n\t\t061894C629A75BEA00E001C2 /* Algorithm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Algorithm.swift; sourceTree = \"<group>\"; };\n\t\t061C17072D1BDA4B00891D66 /* MessageGroupMoreView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageGroupMoreView.swift; sourceTree = \"<group>\"; };\n\t\t061E35852D1E5028009A2D6F /* MessageItemModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageItemModel.swift; sourceTree = \"<group>\"; };\n\t\t0627DABA298B6EA2002F3F69 /* DropBoxView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropBoxView.swift; sourceTree = \"<group>\"; };\n\t\t0627DABC2990D615002F3F69 /* BorderTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BorderTextField.swift; sourceTree = \"<group>\"; };\n\t\t062B98C2251B2762004562E7 /* BKButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BKButton.swift; sourceTree = \"<group>\"; };\n\t\t062B98C7251B27AE004562E7 /* UINavigationItem+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"UINavigationItem+Extension.swift\"; sourceTree = \"<group>\"; };\n\t\t063204F0250B6DD1001561EC /* gotosleep.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = gotosleep.caf; sourceTree = \"<group>\"; };\n\t\t063204F1250B6DD1001561EC /* paymentsuccess.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = paymentsuccess.caf; sourceTree = \"<group>\"; };\n\t\t063204F2250B6DD1001561EC /* shake.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = shake.caf; sourceTree = \"<group>\"; };\n\t\t063204F3250B6DD1001561EC /* alarm.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = alarm.caf; sourceTree = \"<group>\"; };\n\t\t063204F4250B6DD1001561EC /* bloom.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = bloom.caf; sourceTree = \"<group>\"; };\n\t\t063204F5250B6DD2001561EC /* sherwoodforest.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = sherwoodforest.caf; sourceTree = \"<group>\"; };\n\t\t063204F6250B6DD2001561EC /* healthnotification.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = healthnotification.caf; sourceTree = \"<group>\"; };\n\t\t063204F7250B6DD2001561EC /* calypso.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = calypso.caf; sourceTree = \"<group>\"; };\n\t\t063204F8250B6DD2001561EC /* descent.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = descent.caf; sourceTree = \"<group>\"; };\n\t\t063204F9250B6DD2001561EC /* ladder.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = ladder.caf; sourceTree = \"<group>\"; };\n\t\t063204FA250B6DD2001561EC /* tiptoes.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = tiptoes.caf; sourceTree = \"<group>\"; };\n\t\t063204FB250B6DD2001561EC /* fanfare.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = fanfare.caf; sourceTree = \"<group>\"; };\n\t\t063204FC250B6DD2001561EC /* birdsong.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = birdsong.caf; sourceTree = \"<group>\"; };\n\t\t063204FD250B6DD2001561EC /* typewriters.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = typewriters.caf; sourceTree = \"<group>\"; };\n\t\t063204FE250B6DD2001561EC /* anticipate.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = anticipate.caf; sourceTree = \"<group>\"; };\n\t\t063204FF250B6DD3001561EC /* choo.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = choo.caf; sourceTree = \"<group>\"; };\n\t\t06320500250B6DD3001561EC /* glass.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = glass.caf; sourceTree = \"<group>\"; };\n\t\t06320501250B6DD3001561EC /* telegraph.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = telegraph.caf; sourceTree = \"<group>\"; };\n\t\t06320502250B6DD3001561EC /* multiwayinvitation.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = multiwayinvitation.caf; sourceTree = \"<group>\"; };\n\t\t06320503250B6DD3001561EC /* newmail.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = newmail.caf; sourceTree = \"<group>\"; };\n\t\t06320504250B6DD3001561EC /* update.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = update.caf; sourceTree = \"<group>\"; };\n\t\t06320505250B6DD3001561EC /* minuet.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = minuet.caf; sourceTree = \"<group>\"; };\n\t\t06320506250B6DD3001561EC /* suspense.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = suspense.caf; sourceTree = \"<group>\"; };\n\t\t06320507250B6DD3001561EC /* mailsent.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = mailsent.caf; sourceTree = \"<group>\"; };\n\t\t06320508250B6DD3001561EC /* noir.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = noir.caf; sourceTree = \"<group>\"; };\n\t\t06320509250B6DD4001561EC /* chime.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = chime.caf; sourceTree = \"<group>\"; };\n\t\t0632050A250B6DD4001561EC /* spell.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = spell.caf; sourceTree = \"<group>\"; };\n\t\t0632050B250B6DD4001561EC /* electronic.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = electronic.caf; sourceTree = \"<group>\"; };\n\t\t0632050C250B6DD4001561EC /* bell.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = bell.caf; sourceTree = \"<group>\"; };\n\t\t0632050D250B6DD4001561EC /* horn.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = horn.caf; sourceTree = \"<group>\"; };\n\t\t0632050E250B6DD4001561EC /* newsflash.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = newsflash.caf; sourceTree = \"<group>\"; };\n\t\t0632CE1E20EC9098003FDF46 /* NotificationContentExtension.appex */ = {isa = PBXFileReference; explicitFileType = \"wrapper.app-extension\"; includeInIndex = 0; path = NotificationContentExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t0632CE2220EC9098003FDF46 /* NotificationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationViewController.swift; sourceTree = \"<group>\"; };\n\t\t0632CE2520EC9098003FDF46 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = \"<group>\"; };\n\t\t0632CE2720EC9098003FDF46 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = \"<group>\"; };\n\t\t0633E809256A091B00ED0680 /* MJRefresh+Rx.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"MJRefresh+Rx.swift\"; sourceTree = \"<group>\"; };\n\t\t0635A8042CE47DFE0027E00F /* SettingSectionHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingSectionHeader.swift; sourceTree = \"<group>\"; };\n\t\t0635A8062CE4883A0027E00F /* DonateCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DonateCell.swift; sourceTree = \"<group>\"; };\n\t\t0637FA7720E0926D00E80174 /* BarkTargetType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BarkTargetType.swift; sourceTree = \"<group>\"; };\n\t\t0637FA7920E092B300E80174 /* Observable+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"Observable+Extension.swift\"; sourceTree = \"<group>\"; };\n\t\t0637FA7B20E0930E00E80174 /* BarkApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BarkApi.swift; sourceTree = \"<group>\"; };\n\t\t0637FA7D20E0969800E80174 /* Client.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Client.swift; sourceTree = \"<group>\"; };\n\t\t0637FA7F20E0981E00E80174 /* BarkSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BarkSettings.swift; sourceTree = \"<group>\"; };\n\t\t0637FA8120E09C4B00E80174 /* BarkNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BarkNavigationController.swift; sourceTree = \"<group>\"; };\n\t\t0637FA8520E0AB6600E80174 /* UIColor+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"UIColor+Extension.swift\"; sourceTree = \"<group>\"; };\n\t\t0637FA8920E0D58800E80174 /* NewServerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewServerViewController.swift; sourceTree = \"<group>\"; };\n\t\t0637FA8B20E0D7A700E80174 /* BaseViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseViewController.swift; sourceTree = \"<group>\"; };\n\t\t063B909A272149BF00431EC2 /* HomeViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewModelTests.swift; sourceTree = \"<group>\"; };\n\t\t0642B55927EB13F100453D91 /* MutableTextCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutableTextCell.swift; sourceTree = \"<group>\"; };\n\t\t0642B55B27EB149900453D91 /* MutableTextCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutableTextCellViewModel.swift; sourceTree = \"<group>\"; };\n\t\t0647DB672CE604AA00102066 /* MessageSettingFooter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageSettingFooter.swift; sourceTree = \"<group>\"; };\n\t\t064CAB9D256BE9090018155C /* PreviewCardCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewCardCellViewModel.swift; sourceTree = \"<group>\"; };\n\t\t064CABA5256BE9510018155C /* PreviewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewModel.swift; sourceTree = \"<group>\"; };\n\t\t0653677729B727A60038BDB8 /* CryptoSettingRelay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptoSettingRelay.swift; sourceTree = \"<group>\"; };\n\t\t065752C02CB6844A001426D8 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = \"<group>\"; };\n\t\t065AE76A2987777F00323230 /* ArchiveSettingRelay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArchiveSettingRelay.swift; sourceTree = \"<group>\"; };\n\t\t065BE43F2563D649002A8CA4 /* SoundsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoundsViewModel.swift; sourceTree = \"<group>\"; };\n\t\t065BE4452563D7E5002A8CA4 /* ViewModelType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModelType.swift; sourceTree = \"<group>\"; };\n\t\t065BE44A2563D8E1002A8CA4 /* Reusable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Reusable.swift; sourceTree = \"<group>\"; };\n\t\t065BE44F2563D939002A8CA4 /* SoundCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoundCellViewModel.swift; sourceTree = \"<group>\"; };\n\t\t065BE4542565055F002A8CA4 /* HomeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewModel.swift; sourceTree = \"<group>\"; };\n\t\t0660480A2700550600938904 /* Intents.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Intents.framework; path = System/Library/Frameworks/Intents.framework; sourceTree = SDKROOT; };\n\t\t0661A53F204FDA4100965E4E /* Bark.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Bark.app; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t0661A542204FDA4100965E4E /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = \"<group>\"; };\n\t\t0661A544204FDA4100965E4E /* HomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewController.swift; sourceTree = \"<group>\"; };\n\t\t0661A549204FDA4100965E4E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = \"<group>\"; };\n\t\t0661A54C204FDA4100965E4E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = \"<group>\"; };\n\t\t0661A54E204FDA4100965E4E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = \"<group>\"; };\n\t\t0667D191247D162C005DE2ED /* MessageTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageTableViewCell.swift; sourceTree = \"<group>\"; };\n\t\t0667D193247D1BA0005DE2ED /* Date+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"Date+Extension.swift\"; sourceTree = \"<group>\"; };\n\t\t066890072D1946D500E106F2 /* MessageItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageItemView.swift; sourceTree = \"<group>\"; };\n\t\t0668900A2D19525400E106F2 /* ShowLessAndClearView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShowLessAndClearView.swift; sourceTree = \"<group>\"; };\n\t\t0668900C2D19582400E106F2 /* MessageGroupHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageGroupHeaderView.swift; sourceTree = \"<group>\"; };\n\t\t066DF4812D2D31A30092B04E /* MessageDeleteTimeRange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageDeleteTimeRange.swift; sourceTree = \"<group>\"; };\n\t\t066E0C8B2BB6AC9A00873838 /* AddSoundCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddSoundCell.swift; sourceTree = \"<group>\"; };\n\t\t0672CB05256903F700570C9D /* MessageListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageListViewModel.swift; sourceTree = \"<group>\"; };\n\t\t06787C382A710568008ABDD7 /* GesturePassTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GesturePassTextView.swift; sourceTree = \"<group>\"; };\n\t\t067AFB1B2E5D8BE300AE78E7 /* UNNotificationContent+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = \"UNNotificationContent+Extension.swift\"; path = \"notificationContentExtension/UNNotificationContent+Extension.swift\"; sourceTree = SOURCE_ROOT; };\n\t\t067B2EB425693E38008B6BE1 /* MessageSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageSection.swift; sourceTree = \"<group>\"; };\n\t\t0683486A2050F1310024B6DA /* Bark.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Bark.entitlements; sourceTree = \"<group>\"; };\n\t\t0683487020510FB20024B6DA /* UserNotifications.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UserNotifications.framework; path = System/Library/Frameworks/UserNotifications.framework; sourceTree = SDKROOT; };\n\t\t0683487220510FB20024B6DA /* UserNotificationsUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UserNotificationsUI.framework; path = System/Library/Frameworks/UserNotificationsUI.framework; sourceTree = SDKROOT; };\n\t\t06840DBA272298FB001B3193 /* BKColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BKColor.swift; sourceTree = \"<group>\"; };\n\t\t0687F2A72CCB791A00B2A52F /* UIFont+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"UIFont+Extension.swift\"; sourceTree = \"<group>\"; };\n\t\t06885EB5247FB9880004A303 /* MessageSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageSettingsViewController.swift; sourceTree = \"<group>\"; };\n\t\t0689CF4B2C7484A7007203A6 /* BarkTabBarController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BarkTabBarController.swift; sourceTree = \"<group>\"; };\n\t\t068A4B952D2E11CD00982449 /* MessageDeleteTimeRangeTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageDeleteTimeRangeTest.swift; sourceTree = \"<group>\"; };\n\t\t068EC15727ED99C900D5D11E /* ServerListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerListViewController.swift; sourceTree = \"<group>\"; };\n\t\t068EC15927ED99E700D5D11E /* ServerListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerListViewModel.swift; sourceTree = \"<group>\"; };\n\t\t068F66B2247BD84C00DAD25A /* MessageListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageListViewController.swift; sourceTree = \"<group>\"; };\n\t\t0699473C2D223094008D5E40 /* CustomTapTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTapTextView.swift; sourceTree = \"<group>\"; };\n\t\t069C6CA32ED03C72007244BB /* MarkdownProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkdownProcessor.swift; sourceTree = \"<group>\"; };\n\t\t06B1158C247BA6D5006D91FB /* CloudKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CloudKit.framework; path = System/Library/Frameworks/CloudKit.framework; sourceTree = SDKROOT; };\n\t\t06B1158E247BB1FB006D91FB /* Message.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Message.swift; sourceTree = \"<group>\"; };\n\t\t06B11590247BBC15006D91FB /* NotificationServiceExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NotificationServiceExtension.entitlements; sourceTree = \"<group>\"; };\n\t\t06BBB895256518760076F63E /* NewServerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewServerViewModel.swift; sourceTree = \"<group>\"; };\n\t\t06BBB8B62567AC140076F63E /* MessageSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageSettingsViewModel.swift; sourceTree = \"<group>\"; };\n\t\t06BBB8BB2567B3AD0076F63E /* ArchiveSettingCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArchiveSettingCellViewModel.swift; sourceTree = \"<group>\"; };\n\t\t06BBB8C02567B3EF0076F63E /* BaseTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseTableViewCell.swift; sourceTree = \"<group>\"; };\n\t\t06BBB8C82567B6730076F63E /* Operators.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Operators.swift; sourceTree = \"<group>\"; };\n\t\t06BBB8CD2567B8E60076F63E /* silence.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = silence.caf; sourceTree = \"<group>\"; };\n\t\t06BCAE552CDB19260092867A /* GroupMuteSettingManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMuteSettingManager.swift; sourceTree = \"<group>\"; };\n\t\t06BCAE592CDB19590092867A /* NotificationContentExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NotificationContentExtension.entitlements; sourceTree = \"<group>\"; };\n\t\t06BCAE5A2CDB25120092867A /* MuteProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MuteProcessor.swift; sourceTree = \"<group>\"; };\n\t\t06BD4DA92901352E003364DB /* Object+Dictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"Object+Dictionary.swift\"; sourceTree = \"<group>\"; };\n\t\t06BE84032E6EAE7100E6F856 /* InsetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InsetView.swift; sourceTree = \"<group>\"; };\n\t\t06C2CF222685B88D0034B127 /* TextCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextCell.swift; sourceTree = \"<group>\"; };\n\t\t06C2CF242685BDB80034B127 /* SpacerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpacerCell.swift; sourceTree = \"<group>\"; };\n\t\t06C5952C2480E3F8006B98F3 /* LabelCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelCell.swift; sourceTree = \"<group>\"; };\n\t\t06C5953024811392006B98F3 /* ArchiveSettingCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArchiveSettingCell.swift; sourceTree = \"<group>\"; };\n\t\t06C5953224811505006B98F3 /* ArchiveSettingManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArchiveSettingManager.swift; sourceTree = \"<group>\"; };\n\t\t06C595352481160F006B98F3 /* BKLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BKLabel.swift; sourceTree = \"<group>\"; };\n\t\t06C777CF2ECEFF210032A044 /* MarkdownParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkdownParser.swift; sourceTree = \"<group>\"; };\n\t\t06CF784021C7A50300A052D7 /* NotificationServiceExtension.appex */ = {isa = PBXFileReference; explicitFileType = \"wrapper.app-extension\"; includeInIndex = 0; path = NotificationServiceExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t06CF784421C7A50300A052D7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = \"<group>\"; };\n\t\t06CF784B21C7A51200A052D7 /* NotificationService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = \"<group>\"; };\n\t\t06D69E402C11983E00161A35 /* CallProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallProcessor.swift; sourceTree = \"<group>\"; };\n\t\t06E62C102D670F5D004DC82B /* PushToCurrentIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushToCurrentIntent.swift; sourceTree = \"<group>\"; };\n\t\t06E944672C06E40600AC86AB /* NotificationContentProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationContentProcessor.swift; sourceTree = \"<group>\"; };\n\t\t06E944692C06E4A200AC86AB /* CiphertextProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CiphertextProcessor.swift; sourceTree = \"<group>\"; };\n\t\t06E9446C2C06FEC900AC86AB /* LevelProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LevelProcessor.swift; sourceTree = \"<group>\"; };\n\t\t06E9446E2C06FF1E00AC86AB /* BadgeProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgeProcessor.swift; sourceTree = \"<group>\"; };\n\t\t06E944702C06FF4C00AC86AB /* AutoCopyProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoCopyProcessor.swift; sourceTree = \"<group>\"; };\n\t\t06E944722C06FF9200AC86AB /* ArchiveProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArchiveProcessor.swift; sourceTree = \"<group>\"; };\n\t\t06E944742C07012E00AC86AB /* RealmConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RealmConfiguration.swift; sourceTree = \"<group>\"; };\n\t\t06E944772C0701F300AC86AB /* IconProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconProcessor.swift; sourceTree = \"<group>\"; };\n\t\t06E944792C0704E500AC86AB /* ImageDownloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageDownloader.swift; sourceTree = \"<group>\"; };\n\t\t06E9447B2C07052F00AC86AB /* ImageProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageProcessor.swift; sourceTree = \"<group>\"; };\n\t\t06EE1FD026843E9300586708 /* BarkTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BarkTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t06EE1FD226843E9300586708 /* BarkTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BarkTests.swift; sourceTree = \"<group>\"; };\n\t\t06EE1FD426843E9300586708 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = \"<group>\"; };\n\t\t06EEF332291CCFF400CA228A /* CryptoSettingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptoSettingController.swift; sourceTree = \"<group>\"; };\n\t\t06EEF334291CD00000CA228A /* CryptoSettingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptoSettingViewModel.swift; sourceTree = \"<group>\"; };\n\t\t06EF49142D682A96008B91D2 /* PushResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushResponse.swift; sourceTree = \"<group>\"; };\n\t\t06EF49162D682AC3008B91D2 /* OptionsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionsProvider.swift; sourceTree = \"<group>\"; };\n\t\t06EF49182D682B34008B91D2 /* PushToOtherIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushToOtherIntent.swift; sourceTree = \"<group>\"; };\n\t\t06F08EA329B098DD006AB9CA /* CryptoSettingManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptoSettingManager.swift; sourceTree = \"<group>\"; };\n\t\t06F08EA629B1DDFE006AB9CA /* Error+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"Error+Extension.swift\"; sourceTree = \"<group>\"; };\n\t\t06F08EAE29B5D9FF006AB9CA /* HUD.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HUD.swift; sourceTree = \"<group>\"; };\n\t\t06F11E7627D9D5FB00F00298 /* QRScannerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRScannerViewController.swift; sourceTree = \"<group>\"; };\n\t\t06F808902EF4056C0007352A /* AppDelegate+Realm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"AppDelegate+Realm.swift\"; sourceTree = \"<group>\"; };\n\t\t06FB04032C53575400F3A213 /* SharedDefines.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedDefines.swift; sourceTree = \"<group>\"; };\n\t\t121D9B1ED4E8D26F345BC5C0 /* Pods-BarkTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = \"Pods-BarkTests.release.xcconfig\"; path = \"Target Support Files/Pods-BarkTests/Pods-BarkTests.release.xcconfig\"; sourceTree = \"<group>\"; };\n\t\t138CE8CB688587E893BC5C44 /* Pods-Bark.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = \"Pods-Bark.debug.xcconfig\"; path = \"Target Support Files/Pods-Bark/Pods-Bark.debug.xcconfig\"; sourceTree = \"<group>\"; };\n\t\t1E73F99D2C282822002BF649 /* SectionViewController-iPad.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"SectionViewController-iPad.swift\"; sourceTree = \"<group>\"; };\n\t\t1EFB545C2C314A6800B8E51B /* BarkSplitViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BarkSplitViewController.swift; sourceTree = \"<group>\"; };\n\t\t1EFB545E2C32514000B8E51B /* SectionViewModel-iPad.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = \"SectionViewModel-iPad.swift\"; sourceTree = \"<group>\"; };\n\t\t4A9C3B74281C8607B364CED7 /* Pods-NotificationContentExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = \"Pods-NotificationContentExtension.release.xcconfig\"; path = \"Target Support Files/Pods-NotificationContentExtension/Pods-NotificationContentExtension.release.xcconfig\"; sourceTree = \"<group>\"; };\n\t\t519481D715B40109627E1B49 /* Pods-NotificationServiceExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = \"Pods-NotificationServiceExtension.release.xcconfig\"; path = \"Target Support Files/Pods-NotificationServiceExtension/Pods-NotificationServiceExtension.release.xcconfig\"; sourceTree = \"<group>\"; };\n\t\t6616B1ACAA7E422700D90C2B /* libPods-NotificationContentExtension.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = \"libPods-NotificationContentExtension.a\"; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t76381A752CCCD4DA6BB2A566 /* libPods-BarkTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = \"libPods-BarkTests.a\"; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\tA69B47DA6DB3B168D5770B45 /* Pods-Bark.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = \"Pods-Bark.release.xcconfig\"; path = \"Target Support Files/Pods-Bark/Pods-Bark.release.xcconfig\"; sourceTree = \"<group>\"; };\n\t\tB7F8BDFAA047451561798F58 /* libPods-NotificationServiceExtension.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = \"libPods-NotificationServiceExtension.a\"; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\tCCC722470308049D180876C7 /* libPods-Bark.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = \"libPods-Bark.a\"; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\tEF45D2197673129A8128ADB7 /* Pods-NotificationContentExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = \"Pods-NotificationContentExtension.debug.xcconfig\"; path = \"Target Support Files/Pods-NotificationContentExtension/Pods-NotificationContentExtension.debug.xcconfig\"; sourceTree = \"<group>\"; };\n\t\tF20815A821395CCA155806A4 /* Pods-NotificationServiceExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = \"Pods-NotificationServiceExtension.debug.xcconfig\"; path = \"Target Support Files/Pods-NotificationServiceExtension/Pods-NotificationServiceExtension.debug.xcconfig\"; sourceTree = \"<group>\"; };\n\t\tFB59D77AB30F7AD98BA72C3E /* Pods-BarkTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = \"Pods-BarkTests.debug.xcconfig\"; path = \"Target Support Files/Pods-BarkTests/Pods-BarkTests.debug.xcconfig\"; sourceTree = \"<group>\"; };\n/* End PBXFileReference section */\n\n/* Begin PBXFrameworksBuildPhase section */\n\t\t0632CE1B20EC9098003FDF46 /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t0632CE2020EC9098003FDF46 /* UserNotificationsUI.framework in Frameworks */,\n\t\t\t\t0632CE1F20EC9098003FDF46 /* UserNotifications.framework in Frameworks */,\n\t\t\t\t88757E3ACE0EA6A48577C958 /* libPods-NotificationContentExtension.a in Frameworks */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t0661A53C204FDA4100965E4E /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t06C777D32ECF05990032A044 /* Markdown in Frameworks */,\n\t\t\t\t06B1158D247BA6D5006D91FB /* CloudKit.framework in Frameworks */,\n\t\t\t\t3428272069AFAFE2C683FEB0 /* libPods-Bark.a in Frameworks */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t06CF783D21C7A50300A052D7 /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t879AE4D4178855A9672009E4 /* libPods-NotificationServiceExtension.a in Frameworks */,\n\t\t\t\t069C6CA72ED03E27007244BB /* Markdown in Frameworks */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t06EE1FCD26843E9300586708 /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\tB963F7D5BA7AC2571E71EF66 /* libPods-BarkTests.a in Frameworks */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXFrameworksBuildPhase section */\n\n/* Begin PBXGroup section */\n\t\t0604F7DB20620D2700B32F09 /* Controller */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t0637FA8B20E0D7A700E80174 /* BaseViewController.swift */,\n\t\t\t\t0637FA8120E09C4B00E80174 /* BarkNavigationController.swift */,\n\t\t\t\t0603706C20E23EC000F4CA05 /* BarkSFSafariViewController.swift */,\n\t\t\t\t0661A544204FDA4100965E4E /* HomeViewController.swift */,\n\t\t\t\t065BE4542565055F002A8CA4 /* HomeViewModel.swift */,\n\t\t\t\t0637FA8920E0D58800E80174 /* NewServerViewController.swift */,\n\t\t\t\t06BBB895256518760076F63E /* NewServerViewModel.swift */,\n\t\t\t\t068F66B2247BD84C00DAD25A /* MessageListViewController.swift */,\n\t\t\t\t0672CB05256903F700570C9D /* MessageListViewModel.swift */,\n\t\t\t\t06885EB5247FB9880004A303 /* MessageSettingsViewController.swift */,\n\t\t\t\t06BBB8B62567AC140076F63E /* MessageSettingsViewModel.swift */,\n\t\t\t\t060481ED250F404500BC9799 /* SoundsViewController.swift */,\n\t\t\t\t065BE43F2563D649002A8CA4 /* SoundsViewModel.swift */,\n\t\t\t\t06F11E7627D9D5FB00F00298 /* QRScannerViewController.swift */,\n\t\t\t\t068EC15727ED99C900D5D11E /* ServerListViewController.swift */,\n\t\t\t\t068EC15927ED99E700D5D11E /* ServerListViewModel.swift */,\n\t\t\t\t06EEF332291CCFF400CA228A /* CryptoSettingController.swift */,\n\t\t\t\t06EEF334291CD00000CA228A /* CryptoSettingViewModel.swift */,\n\t\t\t\t1E73F99D2C282822002BF649 /* SectionViewController-iPad.swift */,\n\t\t\t\t1EFB545E2C32514000B8E51B /* SectionViewModel-iPad.swift */,\n\t\t\t\t1EFB545C2C314A6800B8E51B /* BarkSplitViewController.swift */,\n\t\t\t\t0689CF4B2C7484A7007203A6 /* BarkTabBarController.swift */,\n\t\t\t);\n\t\t\tpath = Controller;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t0604F7DC20620D3400B32F09 /* View */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t066890092D19495400E106F2 /* MessageList */,\n\t\t\t\t06BBB8C02567B3EF0076F63E /* BaseTableViewCell.swift */,\n\t\t\t\t0603706820E1F89500F4CA05 /* PreviewCardCell.swift */,\n\t\t\t\t064CAB9D256BE9090018155C /* PreviewCardCellViewModel.swift */,\n\t\t\t\t06C5952C2480E3F8006B98F3 /* LabelCell.swift */,\n\t\t\t\t06C5953024811392006B98F3 /* ArchiveSettingCell.swift */,\n\t\t\t\t06BBB8BB2567B3AD0076F63E /* ArchiveSettingCellViewModel.swift */,\n\t\t\t\t060481EF250F51CA00BC9799 /* SoundCell.swift */,\n\t\t\t\t065BE44F2563D939002A8CA4 /* SoundCellViewModel.swift */,\n\t\t\t\t066E0C8B2BB6AC9A00873838 /* AddSoundCell.swift */,\n\t\t\t\t062B98C2251B2762004562E7 /* BKButton.swift */,\n\t\t\t\t06C595352481160F006B98F3 /* BKLabel.swift */,\n\t\t\t\t062B98C7251B27AE004562E7 /* UINavigationItem+Extension.swift */,\n\t\t\t\t06C2CF222685B88D0034B127 /* TextCell.swift */,\n\t\t\t\t06C2CF242685BDB80034B127 /* SpacerCell.swift */,\n\t\t\t\t0642B55927EB13F100453D91 /* MutableTextCell.swift */,\n\t\t\t\t0642B55B27EB149900453D91 /* MutableTextCellViewModel.swift */,\n\t\t\t\t06172FD927F6DAEF002333A4 /* ServerListTableViewCell.swift */,\n\t\t\t\t06172FDB27F6DB06002333A4 /* ServerListTableViewCellViewModel.swift */,\n\t\t\t\t0627DABA298B6EA2002F3F69 /* DropBoxView.swift */,\n\t\t\t\t0627DABC2990D615002F3F69 /* BorderTextField.swift */,\n\t\t\t\t0608F06D2994D115006B8029 /* BKDropDownCell.swift */,\n\t\t\t\t0608F0712994D269006B8029 /* BKDropDownCell.xib */,\n\t\t\t\t061894C429962EB900E001C2 /* GradientButton.swift */,\n\t\t\t\t06F08EAE29B5D9FF006AB9CA /* HUD.swift */,\n\t\t\t\t06787C382A710568008ABDD7 /* GesturePassTextView.swift */,\n\t\t\t\t0635A8042CE47DFE0027E00F /* SettingSectionHeader.swift */,\n\t\t\t\t0635A8062CE4883A0027E00F /* DonateCell.swift */,\n\t\t\t\t0647DB672CE604AA00102066 /* MessageSettingFooter.swift */,\n\t\t\t\t06BE84032E6EAE7100E6F856 /* InsetView.swift */,\n\t\t\t);\n\t\t\tpath = View;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t0604F7DD20620D3800B32F09 /* Model */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t066DF4812D2D31A30092B04E /* MessageDeleteTimeRange.swift */,\n\t\t\t\t06B1158E247BB1FB006D91FB /* Message.swift */,\n\t\t\t\t067B2EB425693E38008B6BE1 /* MessageSection.swift */,\n\t\t\t\t061E35852D1E5028009A2D6F /* MessageItemModel.swift */,\n\t\t\t\t064CABA5256BE9510018155C /* PreviewModel.swift */,\n\t\t\t\t06BD4DA92901352E003364DB /* Object+Dictionary.swift */,\n\t\t\t\t061894C629A75BEA00E001C2 /* Algorithm.swift */,\n\t\t\t);\n\t\t\tpath = Model;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t063204EF250B6DC2001561EC /* Sounds */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t06BBB8CD2567B8E60076F63E /* silence.caf */,\n\t\t\t\t063204F3250B6DD1001561EC /* alarm.caf */,\n\t\t\t\t063204FE250B6DD2001561EC /* anticipate.caf */,\n\t\t\t\t0632050C250B6DD4001561EC /* bell.caf */,\n\t\t\t\t063204FC250B6DD2001561EC /* birdsong.caf */,\n\t\t\t\t063204F4250B6DD1001561EC /* bloom.caf */,\n\t\t\t\t063204F7250B6DD2001561EC /* calypso.caf */,\n\t\t\t\t06320509250B6DD4001561EC /* chime.caf */,\n\t\t\t\t063204FF250B6DD3001561EC /* choo.caf */,\n\t\t\t\t063204F8250B6DD2001561EC /* descent.caf */,\n\t\t\t\t0632050B250B6DD4001561EC /* electronic.caf */,\n\t\t\t\t063204FB250B6DD2001561EC /* fanfare.caf */,\n\t\t\t\t06320500250B6DD3001561EC /* glass.caf */,\n\t\t\t\t063204F0250B6DD1001561EC /* gotosleep.caf */,\n\t\t\t\t063204F6250B6DD2001561EC /* healthnotification.caf */,\n\t\t\t\t0632050D250B6DD4001561EC /* horn.caf */,\n\t\t\t\t063204F9250B6DD2001561EC /* ladder.caf */,\n\t\t\t\t06320507250B6DD3001561EC /* mailsent.caf */,\n\t\t\t\t06320505250B6DD3001561EC /* minuet.caf */,\n\t\t\t\t06320502250B6DD3001561EC /* multiwayinvitation.caf */,\n\t\t\t\t06320503250B6DD3001561EC /* newmail.caf */,\n\t\t\t\t0632050E250B6DD4001561EC /* newsflash.caf */,\n\t\t\t\t06320508250B6DD3001561EC /* noir.caf */,\n\t\t\t\t063204F1250B6DD1001561EC /* paymentsuccess.caf */,\n\t\t\t\t063204F2250B6DD1001561EC /* shake.caf */,\n\t\t\t\t063204F5250B6DD2001561EC /* sherwoodforest.caf */,\n\t\t\t\t0632050A250B6DD4001561EC /* spell.caf */,\n\t\t\t\t06320506250B6DD3001561EC /* suspense.caf */,\n\t\t\t\t06320501250B6DD3001561EC /* telegraph.caf */,\n\t\t\t\t063204FA250B6DD2001561EC /* tiptoes.caf */,\n\t\t\t\t063204FD250B6DD2001561EC /* typewriters.caf */,\n\t\t\t\t06320504250B6DD3001561EC /* update.caf */,\n\t\t\t);\n\t\t\tpath = Sounds;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t0632CE2120EC9098003FDF46 /* NotificationContentExtension */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t06BCAE592CDB19590092867A /* NotificationContentExtension.entitlements */,\n\t\t\t\t0632CE2220EC9098003FDF46 /* NotificationViewController.swift */,\n\t\t\t\t0632CE2420EC9098003FDF46 /* MainInterface.storyboard */,\n\t\t\t\t0632CE2720EC9098003FDF46 /* Info.plist */,\n\t\t\t\t067AFB1B2E5D8BE300AE78E7 /* UNNotificationContent+Extension.swift */,\n\t\t\t);\n\t\t\tpath = NotificationContentExtension;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t065BE4442563D7AA002A8CA4 /* Common */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t065BE45C256507DA002A8CA4 /* Moya */,\n\t\t\t\t065BE4452563D7E5002A8CA4 /* ViewModelType.swift */,\n\t\t\t\t065BE44A2563D8E1002A8CA4 /* Reusable.swift */,\n\t\t\t\t0637FA8520E0AB6600E80174 /* UIColor+Extension.swift */,\n\t\t\t\t0603706620E1E31600F4CA05 /* Defines.swift */,\n\t\t\t\t06FB04032C53575400F3A213 /* SharedDefines.swift */,\n\t\t\t\t0603706A20E20A7C00F4CA05 /* String+Extension.swift */,\n\t\t\t\t0637FA7F20E0981E00E80174 /* BarkSettings.swift */,\n\t\t\t\t0604F7DE20620D4900B32F09 /* ServerManager.swift */,\n\t\t\t\t0637FA7D20E0969800E80174 /* Client.swift */,\n\t\t\t\t0667D193247D1BA0005DE2ED /* Date+Extension.swift */,\n\t\t\t\t06C5953224811505006B98F3 /* ArchiveSettingManager.swift */,\n\t\t\t\t06BBB8C82567B6730076F63E /* Operators.swift */,\n\t\t\t\t0633E809256A091B00ED0680 /* MJRefresh+Rx.swift */,\n\t\t\t\t06840DBA272298FB001B3193 /* BKColor.swift */,\n\t\t\t\t065AE76A2987777F00323230 /* ArchiveSettingRelay.swift */,\n\t\t\t\t06F08EA329B098DD006AB9CA /* CryptoSettingManager.swift */,\n\t\t\t\t0653677729B727A60038BDB8 /* CryptoSettingRelay.swift */,\n\t\t\t\t06F08EA629B1DDFE006AB9CA /* Error+Extension.swift */,\n\t\t\t\t06E944742C07012E00AC86AB /* RealmConfiguration.swift */,\n\t\t\t\t0687F2A72CCB791A00B2A52F /* UIFont+Extension.swift */,\n\t\t\t\t06BCAE552CDB19260092867A /* GroupMuteSettingManager.swift */,\n\t\t\t\t06C777CF2ECEFF210032A044 /* MarkdownParser.swift */,\n\t\t\t);\n\t\t\tpath = Common;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t065BE45C256507DA002A8CA4 /* Moya */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t0637FA7720E0926D00E80174 /* BarkTargetType.swift */,\n\t\t\t\t0637FA7920E092B300E80174 /* Observable+Extension.swift */,\n\t\t\t\t0637FA7B20E0930E00E80174 /* BarkApi.swift */,\n\t\t\t);\n\t\t\tpath = Moya;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t0661A536204FDA4100965E4E = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t063204EF250B6DC2001561EC /* Sounds */,\n\t\t\t\t065BE4442563D7AA002A8CA4 /* Common */,\n\t\t\t\t0604F7DD20620D3800B32F09 /* Model */,\n\t\t\t\t0604F7DC20620D3400B32F09 /* View */,\n\t\t\t\t0604F7DB20620D2700B32F09 /* Controller */,\n\t\t\t\t0661A541204FDA4100965E4E /* Bark */,\n\t\t\t\t0632CE2120EC9098003FDF46 /* NotificationContentExtension */,\n\t\t\t\t06CF784121C7A50300A052D7 /* NotificationServiceExtension */,\n\t\t\t\t06EE1FD126843E9300586708 /* BarkTests */,\n\t\t\t\t0661A540204FDA4100965E4E /* Products */,\n\t\t\t\t99BD309BDB7F62B5DC0CECE1 /* Frameworks */,\n\t\t\t\tE9062CB15F1B70C3719578FB /* Pods */,\n\t\t\t);\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t0661A540204FDA4100965E4E /* Products */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t0661A53F204FDA4100965E4E /* Bark.app */,\n\t\t\t\t0632CE1E20EC9098003FDF46 /* NotificationContentExtension.appex */,\n\t\t\t\t06CF784021C7A50300A052D7 /* NotificationServiceExtension.appex */,\n\t\t\t\t06EE1FD026843E9300586708 /* BarkTests.xctest */,\n\t\t\t);\n\t\t\tname = Products;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t0661A541204FDA4100965E4E /* Bark */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t06EF49132D682A82008B91D2 /* Intents */,\n\t\t\t\t0683486A2050F1310024B6DA /* Bark.entitlements */,\n\t\t\t\t0661A542204FDA4100965E4E /* AppDelegate.swift */,\n\t\t\t\t06F808902EF4056C0007352A /* AppDelegate+Realm.swift */,\n\t\t\t\t0661A549204FDA4100965E4E /* Assets.xcassets */,\n\t\t\t\t0661A54B204FDA4100965E4E /* LaunchScreen.storyboard */,\n\t\t\t\t0661A54E204FDA4100965E4E /* Info.plist */,\n\t\t\t\t065752C02CB6844A001426D8 /* Localizable.xcstrings */,\n\t\t\t);\n\t\t\tpath = Bark;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t066890092D19495400E106F2 /* MessageList */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t0699473C2D223094008D5E40 /* CustomTapTextView.swift */,\n\t\t\t\t066890072D1946D500E106F2 /* MessageItemView.swift */,\n\t\t\t\t0667D191247D162C005DE2ED /* MessageTableViewCell.swift */,\n\t\t\t\t0668900A2D19525400E106F2 /* ShowLessAndClearView.swift */,\n\t\t\t\t0668900C2D19582400E106F2 /* MessageGroupHeaderView.swift */,\n\t\t\t\t061C17072D1BDA4B00891D66 /* MessageGroupMoreView.swift */,\n\t\t\t);\n\t\t\tpath = MessageList;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t06CF784121C7A50300A052D7 /* NotificationServiceExtension */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t06E9446B2C06E4A800AC86AB /* Processor */,\n\t\t\t\t06B11590247BBC15006D91FB /* NotificationServiceExtension.entitlements */,\n\t\t\t\t06CF784B21C7A51200A052D7 /* NotificationService.swift */,\n\t\t\t\t06CF784421C7A50300A052D7 /* Info.plist */,\n\t\t\t);\n\t\t\tpath = NotificationServiceExtension;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t06E9446B2C06E4A800AC86AB /* Processor */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t06E944672C06E40600AC86AB /* NotificationContentProcessor.swift */,\n\t\t\t\t06E944692C06E4A200AC86AB /* CiphertextProcessor.swift */,\n\t\t\t\t06E9446C2C06FEC900AC86AB /* LevelProcessor.swift */,\n\t\t\t\t06E9446E2C06FF1E00AC86AB /* BadgeProcessor.swift */,\n\t\t\t\t06E944702C06FF4C00AC86AB /* AutoCopyProcessor.swift */,\n\t\t\t\t06E944722C06FF9200AC86AB /* ArchiveProcessor.swift */,\n\t\t\t\t06E944772C0701F300AC86AB /* IconProcessor.swift */,\n\t\t\t\t06E9447B2C07052F00AC86AB /* ImageProcessor.swift */,\n\t\t\t\t06D69E402C11983E00161A35 /* CallProcessor.swift */,\n\t\t\t\t06E944792C0704E500AC86AB /* ImageDownloader.swift */,\n\t\t\t\t06BCAE5A2CDB25120092867A /* MuteProcessor.swift */,\n\t\t\t\t069C6CA32ED03C72007244BB /* MarkdownProcessor.swift */,\n\t\t\t);\n\t\t\tpath = Processor;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t06EE1FD126843E9300586708 /* BarkTests */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t06EE1FD226843E9300586708 /* BarkTests.swift */,\n\t\t\t\t06EE1FD426843E9300586708 /* Info.plist */,\n\t\t\t\t063B909A272149BF00431EC2 /* HomeViewModelTests.swift */,\n\t\t\t\t068A4B952D2E11CD00982449 /* MessageDeleteTimeRangeTest.swift */,\n\t\t\t);\n\t\t\tpath = BarkTests;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t06EF49132D682A82008B91D2 /* Intents */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t06EF49162D682AC3008B91D2 /* OptionsProvider.swift */,\n\t\t\t\t06EF49142D682A96008B91D2 /* PushResponse.swift */,\n\t\t\t\t06E62C102D670F5D004DC82B /* PushToCurrentIntent.swift */,\n\t\t\t\t06EF49182D682B34008B91D2 /* PushToOtherIntent.swift */,\n\t\t\t);\n\t\t\tpath = Intents;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t99BD309BDB7F62B5DC0CECE1 /* Frameworks */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t06B1158C247BA6D5006D91FB /* CloudKit.framework */,\n\t\t\t\t0683487020510FB20024B6DA /* UserNotifications.framework */,\n\t\t\t\t0683487220510FB20024B6DA /* UserNotificationsUI.framework */,\n\t\t\t\t0660480A2700550600938904 /* Intents.framework */,\n\t\t\t\tCCC722470308049D180876C7 /* libPods-Bark.a */,\n\t\t\t\t76381A752CCCD4DA6BB2A566 /* libPods-BarkTests.a */,\n\t\t\t\tB7F8BDFAA047451561798F58 /* libPods-NotificationServiceExtension.a */,\n\t\t\t\t6616B1ACAA7E422700D90C2B /* libPods-NotificationContentExtension.a */,\n\t\t\t);\n\t\t\tname = Frameworks;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tE9062CB15F1B70C3719578FB /* Pods */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t138CE8CB688587E893BC5C44 /* Pods-Bark.debug.xcconfig */,\n\t\t\t\tA69B47DA6DB3B168D5770B45 /* Pods-Bark.release.xcconfig */,\n\t\t\t\tFB59D77AB30F7AD98BA72C3E /* Pods-BarkTests.debug.xcconfig */,\n\t\t\t\t121D9B1ED4E8D26F345BC5C0 /* Pods-BarkTests.release.xcconfig */,\n\t\t\t\tF20815A821395CCA155806A4 /* Pods-NotificationServiceExtension.debug.xcconfig */,\n\t\t\t\t519481D715B40109627E1B49 /* Pods-NotificationServiceExtension.release.xcconfig */,\n\t\t\t\tEF45D2197673129A8128ADB7 /* Pods-NotificationContentExtension.debug.xcconfig */,\n\t\t\t\t4A9C3B74281C8607B364CED7 /* Pods-NotificationContentExtension.release.xcconfig */,\n\t\t\t);\n\t\t\tpath = Pods;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n/* End PBXGroup section */\n\n/* Begin PBXNativeTarget section */\n\t\t0632CE1D20EC9098003FDF46 /* NotificationContentExtension */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = 0632CE2D20EC9098003FDF46 /* Build configuration list for PBXNativeTarget \"NotificationContentExtension\" */;\n\t\t\tbuildPhases = (\n\t\t\t\t0B84A6EDEEC0D92E4E6CAFF1 /* [CP] Check Pods Manifest.lock */,\n\t\t\t\t0632CE1A20EC9098003FDF46 /* Sources */,\n\t\t\t\t0632CE1B20EC9098003FDF46 /* Frameworks */,\n\t\t\t\t0632CE1C20EC9098003FDF46 /* Resources */,\n\t\t\t\t16C0395E271EF88C500FD38B /* [CP] Copy Pods Resources */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t);\n\t\t\tname = NotificationContentExtension;\n\t\t\tproductName = NotificationContentExtension;\n\t\t\tproductReference = 0632CE1E20EC9098003FDF46 /* NotificationContentExtension.appex */;\n\t\t\tproductType = \"com.apple.product-type.app-extension\";\n\t\t};\n\t\t0661A53E204FDA4100965E4E /* Bark */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = 0661A551204FDA4100965E4E /* Build configuration list for PBXNativeTarget \"Bark\" */;\n\t\t\tbuildPhases = (\n\t\t\t\t96E966B1F9EF139B935A8BB1 /* [CP] Check Pods Manifest.lock */,\n\t\t\t\t0661A53B204FDA4100965E4E /* Sources */,\n\t\t\t\t0661A53C204FDA4100965E4E /* Frameworks */,\n\t\t\t\t0661A53D204FDA4100965E4E /* Resources */,\n\t\t\t\t0632CE2E20EC9098003FDF46 /* Embed Foundation Extensions */,\n\t\t\t\t330C2B4092D535691BE03E9D /* [CP] Copy Pods Resources */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t\t0632CE2920EC9098003FDF46 /* PBXTargetDependency */,\n\t\t\t\t06CF784621C7A50300A052D7 /* PBXTargetDependency */,\n\t\t\t);\n\t\t\tname = Bark;\n\t\t\tproductName = Bark;\n\t\t\tproductReference = 0661A53F204FDA4100965E4E /* Bark.app */;\n\t\t\tproductType = \"com.apple.product-type.application\";\n\t\t};\n\t\t06CF783F21C7A50300A052D7 /* NotificationServiceExtension */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = 06CF784A21C7A50300A052D7 /* Build configuration list for PBXNativeTarget \"NotificationServiceExtension\" */;\n\t\t\tbuildPhases = (\n\t\t\t\t02CA273651C315B3AC3557D7 /* [CP] Check Pods Manifest.lock */,\n\t\t\t\t06CF783C21C7A50300A052D7 /* Sources */,\n\t\t\t\t06CF783D21C7A50300A052D7 /* Frameworks */,\n\t\t\t\t06CF783E21C7A50300A052D7 /* Resources */,\n\t\t\t\t73BB82AB466F692F871A6876 /* [CP] Copy Pods Resources */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t);\n\t\t\tname = NotificationServiceExtension;\n\t\t\tproductName = NotificationServiceExtension;\n\t\t\tproductReference = 06CF784021C7A50300A052D7 /* NotificationServiceExtension.appex */;\n\t\t\tproductType = \"com.apple.product-type.app-extension\";\n\t\t};\n\t\t06EE1FCF26843E9300586708 /* BarkTests */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = 06EE1FD726843E9300586708 /* Build configuration list for PBXNativeTarget \"BarkTests\" */;\n\t\t\tbuildPhases = (\n\t\t\t\tFFCCE5E170E29FF10836E9C0 /* [CP] Check Pods Manifest.lock */,\n\t\t\t\t06EE1FCC26843E9300586708 /* Sources */,\n\t\t\t\t06EE1FCD26843E9300586708 /* Frameworks */,\n\t\t\t\t06EE1FCE26843E9300586708 /* Resources */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t\t06EE1FD626843E9300586708 /* PBXTargetDependency */,\n\t\t\t);\n\t\t\tname = BarkTests;\n\t\t\tproductName = BarkTests;\n\t\t\tproductReference = 06EE1FD026843E9300586708 /* BarkTests.xctest */;\n\t\t\tproductType = \"com.apple.product-type.bundle.unit-test\";\n\t\t};\n/* End PBXNativeTarget section */\n\n/* Begin PBXProject section */\n\t\t0661A537204FDA4100965E4E /* Project object */ = {\n\t\t\tisa = PBXProject;\n\t\t\tattributes = {\n\t\t\t\tBuildIndependentTargetsInParallel = YES;\n\t\t\t\tLastSwiftUpdateCheck = 1300;\n\t\t\t\tLastUpgradeCheck = 1640;\n\t\t\t\tORGANIZATIONNAME = Fin;\n\t\t\t\tTargetAttributes = {\n\t\t\t\t\t0632CE1D20EC9098003FDF46 = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 9.3;\n\t\t\t\t\t\tProvisioningStyle = Manual;\n\t\t\t\t\t};\n\t\t\t\t\t0661A53E204FDA4100965E4E = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 9.2;\n\t\t\t\t\t\tProvisioningStyle = Manual;\n\t\t\t\t\t\tSystemCapabilities = {\n\t\t\t\t\t\t\tcom.apple.Push = {\n\t\t\t\t\t\t\t\tenabled = 1;\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t};\n\t\t\t\t\t};\n\t\t\t\t\t06CF783F21C7A50300A052D7 = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 10.1;\n\t\t\t\t\t\tProvisioningStyle = Manual;\n\t\t\t\t\t};\n\t\t\t\t\t06EE1FCF26843E9300586708 = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 12.5;\n\t\t\t\t\t\tProvisioningStyle = Automatic;\n\t\t\t\t\t\tTestTargetID = 0661A53E204FDA4100965E4E;\n\t\t\t\t\t};\n\t\t\t\t};\n\t\t\t};\n\t\t\tbuildConfigurationList = 0661A53A204FDA4100965E4E /* Build configuration list for PBXProject \"Bark\" */;\n\t\t\tcompatibilityVersion = \"Xcode 8.0\";\n\t\t\tdevelopmentRegion = en;\n\t\t\thasScannedForEncodings = 0;\n\t\t\tknownRegions = (\n\t\t\t\ten,\n\t\t\t\tBase,\n\t\t\t\t\"zh-Hans\",\n\t\t\t\ttr,\n\t\t\t\tja,\n\t\t\t);\n\t\t\tmainGroup = 0661A536204FDA4100965E4E;\n\t\t\tpackageReferences = (\n\t\t\t\t06C777D12ECF05990032A044 /* XCRemoteSwiftPackageReference \"swift-markdown\" */,\n\t\t\t);\n\t\t\tproductRefGroup = 0661A540204FDA4100965E4E /* Products */;\n\t\t\tprojectDirPath = \"\";\n\t\t\tprojectRoot = \"\";\n\t\t\ttargets = (\n\t\t\t\t0661A53E204FDA4100965E4E /* Bark */,\n\t\t\t\t0632CE1D20EC9098003FDF46 /* NotificationContentExtension */,\n\t\t\t\t06CF783F21C7A50300A052D7 /* NotificationServiceExtension */,\n\t\t\t\t06EE1FCF26843E9300586708 /* BarkTests */,\n\t\t\t);\n\t\t};\n/* End PBXProject section */\n\n/* Begin PBXResourcesBuildPhase section */\n\t\t0632CE1C20EC9098003FDF46 /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t06802E5320ECC40C00767047 /* Assets.xcassets in Resources */,\n\t\t\t\t065752C22CB6844A001426D8 /* Localizable.xcstrings in Resources */,\n\t\t\t\t0632CE2620EC9098003FDF46 /* MainInterface.storyboard in Resources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t0661A53D204FDA4100965E4E /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t06320525250B6DD4001561EC /* suspense.caf in Resources */,\n\t\t\t\t06320529250B6DD4001561EC /* spell.caf in Resources */,\n\t\t\t\t065752C12CB6844A001426D8 /* Localizable.xcstrings in Resources */,\n\t\t\t\t0632051D250B6DD4001561EC /* anticipate.caf in Resources */,\n\t\t\t\t06320521250B6DD4001561EC /* multiwayinvitation.caf in Resources */,\n\t\t\t\t06BBB8CE2567B8E60076F63E /* silence.caf in Resources */,\n\t\t\t\t06320528250B6DD4001561EC /* chime.caf in Resources */,\n\t\t\t\t06320510250B6DD4001561EC /* paymentsuccess.caf in Resources */,\n\t\t\t\t06320527250B6DD4001561EC /* noir.caf in Resources */,\n\t\t\t\t06320520250B6DD4001561EC /* telegraph.caf in Resources */,\n\t\t\t\t06320518250B6DD4001561EC /* ladder.caf in Resources */,\n\t\t\t\t06320526250B6DD4001561EC /* mailsent.caf in Resources */,\n\t\t\t\t06320523250B6DD4001561EC /* update.caf in Resources */,\n\t\t\t\t06320515250B6DD4001561EC /* healthnotification.caf in Resources */,\n\t\t\t\t06320511250B6DD4001561EC /* shake.caf in Resources */,\n\t\t\t\t06320513250B6DD4001561EC /* bloom.caf in Resources */,\n\t\t\t\t0632050F250B6DD4001561EC /* gotosleep.caf in Resources */,\n\t\t\t\t06320512250B6DD4001561EC /* alarm.caf in Resources */,\n\t\t\t\t06320522250B6DD4001561EC /* newmail.caf in Resources */,\n\t\t\t\t0661A54D204FDA4100965E4E /* LaunchScreen.storyboard in Resources */,\n\t\t\t\t0632051E250B6DD4001561EC /* choo.caf in Resources */,\n\t\t\t\t06320516250B6DD4001561EC /* calypso.caf in Resources */,\n\t\t\t\t0608F0722994D269006B8029 /* BKDropDownCell.xib in Resources */,\n\t\t\t\t0632052D250B6DD4001561EC /* newsflash.caf in Resources */,\n\t\t\t\t06320514250B6DD4001561EC /* sherwoodforest.caf in Resources */,\n\t\t\t\t0632051A250B6DD4001561EC /* fanfare.caf in Resources */,\n\t\t\t\t06320517250B6DD4001561EC /* descent.caf in Resources */,\n\t\t\t\t0632051B250B6DD4001561EC /* birdsong.caf in Resources */,\n\t\t\t\t0632051F250B6DD4001561EC /* glass.caf in Resources */,\n\t\t\t\t06320524250B6DD4001561EC /* minuet.caf in Resources */,\n\t\t\t\t0661A54A204FDA4100965E4E /* Assets.xcassets in Resources */,\n\t\t\t\t06320519250B6DD4001561EC /* tiptoes.caf in Resources */,\n\t\t\t\t0632052A250B6DD4001561EC /* electronic.caf in Resources */,\n\t\t\t\t0632052C250B6DD4001561EC /* horn.caf in Resources */,\n\t\t\t\t0632051C250B6DD4001561EC /* typewriters.caf in Resources */,\n\t\t\t\t0632052B250B6DD4001561EC /* bell.caf in Resources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t06CF783E21C7A50300A052D7 /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t06D69E202C1159E200161A35 /* glass.caf in Resources */,\n\t\t\t\t06D69E212C1159E200161A35 /* sherwoodforest.caf in Resources */,\n\t\t\t\t06D69E222C1159E200161A35 /* ladder.caf in Resources */,\n\t\t\t\t06D69E232C1159E200161A35 /* chime.caf in Resources */,\n\t\t\t\t06D69E242C1159E200161A35 /* anticipate.caf in Resources */,\n\t\t\t\t06D69E252C1159E200161A35 /* update.caf in Resources */,\n\t\t\t\t06D69E262C1159E200161A35 /* suspense.caf in Resources */,\n\t\t\t\t06D69E272C1159E200161A35 /* newmail.caf in Resources */,\n\t\t\t\t06D69E282C1159E200161A35 /* noir.caf in Resources */,\n\t\t\t\t06D69E292C1159E200161A35 /* birdsong.caf in Resources */,\n\t\t\t\t06D69E2A2C1159E200161A35 /* minuet.caf in Resources */,\n\t\t\t\t06D69E2B2C1159E200161A35 /* shake.caf in Resources */,\n\t\t\t\t06D69E2C2C1159E200161A35 /* newsflash.caf in Resources */,\n\t\t\t\t06D69E2D2C1159E200161A35 /* paymentsuccess.caf in Resources */,\n\t\t\t\t06D69E2E2C1159E200161A35 /* descent.caf in Resources */,\n\t\t\t\t06D69E2F2C1159E200161A35 /* mailsent.caf in Resources */,\n\t\t\t\t06D69E302C1159E200161A35 /* tiptoes.caf in Resources */,\n\t\t\t\t065752C32CB6844A001426D8 /* Localizable.xcstrings in Resources */,\n\t\t\t\t06D69E312C1159E200161A35 /* telegraph.caf in Resources */,\n\t\t\t\t06D69E322C1159E200161A35 /* healthnotification.caf in Resources */,\n\t\t\t\t06D69E332C1159E200161A35 /* typewriters.caf in Resources */,\n\t\t\t\t06D69E342C1159E200161A35 /* bell.caf in Resources */,\n\t\t\t\t06D69E352C1159E200161A35 /* bloom.caf in Resources */,\n\t\t\t\t06D69E362C1159E200161A35 /* spell.caf in Resources */,\n\t\t\t\t06D69E372C1159E200161A35 /* choo.caf in Resources */,\n\t\t\t\t06D69E382C1159E200161A35 /* multiwayinvitation.caf in Resources */,\n\t\t\t\t06D69E392C1159E200161A35 /* horn.caf in Resources */,\n\t\t\t\t06D69E3A2C1159E200161A35 /* electronic.caf in Resources */,\n\t\t\t\t06D69E3B2C1159E200161A35 /* calypso.caf in Resources */,\n\t\t\t\t06D69E3C2C1159E200161A35 /* gotosleep.caf in Resources */,\n\t\t\t\t06D69E3D2C1159E200161A35 /* silence.caf in Resources */,\n\t\t\t\t06D69E3E2C1159E200161A35 /* alarm.caf in Resources */,\n\t\t\t\t06D69E3F2C1159E200161A35 /* fanfare.caf in Resources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t06EE1FCE26843E9300586708 /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXResourcesBuildPhase section */\n\n/* Begin PBXShellScriptBuildPhase section */\n\t\t02CA273651C315B3AC3557D7 /* [CP] Check Pods Manifest.lock */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputFileListPaths = (\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t\t\"${PODS_PODFILE_DIR_PATH}/Podfile.lock\",\n\t\t\t\t\"${PODS_ROOT}/Manifest.lock\",\n\t\t\t);\n\t\t\tname = \"[CP] Check Pods Manifest.lock\";\n\t\t\toutputFileListPaths = (\n\t\t\t);\n\t\t\toutputPaths = (\n\t\t\t\t\"$(DERIVED_FILE_DIR)/Pods-NotificationServiceExtension-checkManifestLockResult.txt\",\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"diff \\\"${PODS_PODFILE_DIR_PATH}/Podfile.lock\\\" \\\"${PODS_ROOT}/Manifest.lock\\\" > /dev/null\\nif [ $? != 0 ] ; then\\n    # print error to STDERR\\n    echo \\\"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\\\" >&2\\n    exit 1\\nfi\\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\\necho \\\"SUCCESS\\\" > \\\"${SCRIPT_OUTPUT_FILE_0}\\\"\\n\";\n\t\t\tshowEnvVarsInLog = 0;\n\t\t};\n\t\t0B84A6EDEEC0D92E4E6CAFF1 /* [CP] Check Pods Manifest.lock */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputFileListPaths = (\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t\t\"${PODS_PODFILE_DIR_PATH}/Podfile.lock\",\n\t\t\t\t\"${PODS_ROOT}/Manifest.lock\",\n\t\t\t);\n\t\t\tname = \"[CP] Check Pods Manifest.lock\";\n\t\t\toutputFileListPaths = (\n\t\t\t);\n\t\t\toutputPaths = (\n\t\t\t\t\"$(DERIVED_FILE_DIR)/Pods-NotificationContentExtension-checkManifestLockResult.txt\",\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"diff \\\"${PODS_PODFILE_DIR_PATH}/Podfile.lock\\\" \\\"${PODS_ROOT}/Manifest.lock\\\" > /dev/null\\nif [ $? != 0 ] ; then\\n    # print error to STDERR\\n    echo \\\"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\\\" >&2\\n    exit 1\\nfi\\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\\necho \\\"SUCCESS\\\" > \\\"${SCRIPT_OUTPUT_FILE_0}\\\"\\n\";\n\t\t\tshowEnvVarsInLog = 0;\n\t\t};\n\t\t16C0395E271EF88C500FD38B /* [CP] Copy Pods Resources */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t\t\"${PODS_ROOT}/Target Support Files/Pods-NotificationContentExtension/Pods-NotificationContentExtension-resources.sh\",\n\t\t\t\t\"${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher/Kingfisher.bundle\",\n\t\t\t);\n\t\t\tname = \"[CP] Copy Pods Resources\";\n\t\t\toutputPaths = (\n\t\t\t\t\"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Kingfisher.bundle\",\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"\\\"${PODS_ROOT}/Target Support Files/Pods-NotificationContentExtension/Pods-NotificationContentExtension-resources.sh\\\"\\n\";\n\t\t\tshowEnvVarsInLog = 0;\n\t\t};\n\t\t330C2B4092D535691BE03E9D /* [CP] Copy Pods Resources */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t\t\"${PODS_ROOT}/Target Support Files/Pods-Bark/Pods-Bark-resources.sh\",\n\t\t\t\t\"${PODS_CONFIGURATION_BUILD_DIR}/Alamofire/Alamofire.bundle\",\n\t\t\t\t\"${PODS_CONFIGURATION_BUILD_DIR}/CryptoSwift/CryptoSwift.bundle\",\n\t\t\t\t\"${PODS_ROOT}/DropDown/DropDown/resources/DropDownCell.xib\",\n\t\t\t\t\"${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore/IQKeyboardCore.bundle\",\n\t\t\t\t\"${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification/IQKeyboardNotification.bundle\",\n\t\t\t\t\"${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar/IQKeyboardToolbar.bundle\",\n\t\t\t\t\"${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager/IQKeyboardToolbarManager.bundle\",\n\t\t\t\t\"${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification/IQTextInputViewNotification.bundle\",\n\t\t\t\t\"${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher/Kingfisher.bundle\",\n\t\t\t\t\"${PODS_ROOT}/MJRefresh/MJRefresh/MJRefresh.bundle\",\n\t\t\t\t\"${PODS_CONFIGURATION_BUILD_DIR}/MJRefresh/MJRefresh.Privacy.bundle\",\n\t\t\t\t\"${PODS_CONFIGURATION_BUILD_DIR}/Material/com.cosmicmind.material.icons.bundle\",\n\t\t\t\t\"${PODS_CONFIGURATION_BUILD_DIR}/Material/com.cosmicmind.material.fonts.bundle\",\n\t\t\t\t\"${PODS_CONFIGURATION_BUILD_DIR}/MercariQRScanner/QRScannerAssets.bundle\",\n\t\t\t\t\"${PODS_CONFIGURATION_BUILD_DIR}/ObjectMapper/Privacy.bundle\",\n\t\t\t\t\"${PODS_CONFIGURATION_BUILD_DIR}/Realm/realm_objc_privacy.bundle\",\n\t\t\t\t\"${PODS_CONFIGURATION_BUILD_DIR}/RealmSwift/realm_swift_privacy.bundle\",\n\t\t\t\t\"${PODS_CONFIGURATION_BUILD_DIR}/RxCocoa/RxCocoa_Privacy.bundle\",\n\t\t\t\t\"${PODS_CONFIGURATION_BUILD_DIR}/RxRelay/RxRelay_Privacy.bundle\",\n\t\t\t\t\"${PODS_CONFIGURATION_BUILD_DIR}/RxSwift/RxSwift_Privacy.bundle\",\n\t\t\t\t\"${PODS_ROOT}/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle\",\n\t\t\t\t\"${PODS_ROOT}/SVProgressHUD/SVProgressHUD/PrivacyInfo.xcprivacy\",\n\t\t\t\t\"${PODS_CONFIGURATION_BUILD_DIR}/SnapKit/SnapKit_Privacy.bundle\",\n\t\t\t\t\"${PODS_CONFIGURATION_BUILD_DIR}/SwiftyJSON/SwiftyJSON.bundle\",\n\t\t\t);\n\t\t\tname = \"[CP] Copy Pods Resources\";\n\t\t\toutputPaths = (\n\t\t\t\t\"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Alamofire.bundle\",\n\t\t\t\t\"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/CryptoSwift.bundle\",\n\t\t\t\t\"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/DropDownCell.nib\",\n\t\t\t\t\"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/IQKeyboardCore.bundle\",\n\t\t\t\t\"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/IQKeyboardNotification.bundle\",\n\t\t\t\t\"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/IQKeyboardToolbar.bundle\",\n\t\t\t\t\"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/IQKeyboardToolbarManager.bundle\",\n\t\t\t\t\"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/IQTextInputViewNotification.bundle\",\n\t\t\t\t\"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Kingfisher.bundle\",\n\t\t\t\t\"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/MJRefresh.bundle\",\n\t\t\t\t\"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/MJRefresh.Privacy.bundle\",\n\t\t\t\t\"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/com.cosmicmind.material.icons.bundle\",\n\t\t\t\t\"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/com.cosmicmind.material.fonts.bundle\",\n\t\t\t\t\"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/QRScannerAssets.bundle\",\n\t\t\t\t\"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Privacy.bundle\",\n\t\t\t\t\"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/realm_objc_privacy.bundle\",\n\t\t\t\t\"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/realm_swift_privacy.bundle\",\n\t\t\t\t\"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RxCocoa_Privacy.bundle\",\n\t\t\t\t\"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RxRelay_Privacy.bundle\",\n\t\t\t\t\"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RxSwift_Privacy.bundle\",\n\t\t\t\t\"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/SVProgressHUD.bundle\",\n\t\t\t\t\"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/PrivacyInfo.xcprivacy\",\n\t\t\t\t\"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/SnapKit_Privacy.bundle\",\n\t\t\t\t\"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/SwiftyJSON.bundle\",\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"\\\"${PODS_ROOT}/Target Support Files/Pods-Bark/Pods-Bark-resources.sh\\\"\\n\";\n\t\t\tshowEnvVarsInLog = 0;\n\t\t};\n\t\t73BB82AB466F692F871A6876 /* [CP] Copy Pods Resources */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t\t\"${PODS_ROOT}/Target Support Files/Pods-NotificationServiceExtension/Pods-NotificationServiceExtension-resources.sh\",\n\t\t\t\t\"${PODS_CONFIGURATION_BUILD_DIR}/CryptoSwift/CryptoSwift.bundle\",\n\t\t\t\t\"${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher/Kingfisher.bundle\",\n\t\t\t\t\"${PODS_CONFIGURATION_BUILD_DIR}/Realm/realm_objc_privacy.bundle\",\n\t\t\t\t\"${PODS_CONFIGURATION_BUILD_DIR}/RealmSwift/realm_swift_privacy.bundle\",\n\t\t\t\t\"${PODS_CONFIGURATION_BUILD_DIR}/SwiftyJSON/SwiftyJSON.bundle\",\n\t\t\t);\n\t\t\tname = \"[CP] Copy Pods Resources\";\n\t\t\toutputPaths = (\n\t\t\t\t\"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/CryptoSwift.bundle\",\n\t\t\t\t\"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Kingfisher.bundle\",\n\t\t\t\t\"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/realm_objc_privacy.bundle\",\n\t\t\t\t\"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/realm_swift_privacy.bundle\",\n\t\t\t\t\"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/SwiftyJSON.bundle\",\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"\\\"${PODS_ROOT}/Target Support Files/Pods-NotificationServiceExtension/Pods-NotificationServiceExtension-resources.sh\\\"\\n\";\n\t\t\tshowEnvVarsInLog = 0;\n\t\t};\n\t\t96E966B1F9EF139B935A8BB1 /* [CP] Check Pods Manifest.lock */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputFileListPaths = (\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t\t\"${PODS_PODFILE_DIR_PATH}/Podfile.lock\",\n\t\t\t\t\"${PODS_ROOT}/Manifest.lock\",\n\t\t\t);\n\t\t\tname = \"[CP] Check Pods Manifest.lock\";\n\t\t\toutputFileListPaths = (\n\t\t\t);\n\t\t\toutputPaths = (\n\t\t\t\t\"$(DERIVED_FILE_DIR)/Pods-Bark-checkManifestLockResult.txt\",\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"diff \\\"${PODS_PODFILE_DIR_PATH}/Podfile.lock\\\" \\\"${PODS_ROOT}/Manifest.lock\\\" > /dev/null\\nif [ $? != 0 ] ; then\\n    # print error to STDERR\\n    echo \\\"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\\\" >&2\\n    exit 1\\nfi\\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\\necho \\\"SUCCESS\\\" > \\\"${SCRIPT_OUTPUT_FILE_0}\\\"\\n\";\n\t\t\tshowEnvVarsInLog = 0;\n\t\t};\n\t\tFFCCE5E170E29FF10836E9C0 /* [CP] Check Pods Manifest.lock */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputFileListPaths = (\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t\t\"${PODS_PODFILE_DIR_PATH}/Podfile.lock\",\n\t\t\t\t\"${PODS_ROOT}/Manifest.lock\",\n\t\t\t);\n\t\t\tname = \"[CP] Check Pods Manifest.lock\";\n\t\t\toutputFileListPaths = (\n\t\t\t);\n\t\t\toutputPaths = (\n\t\t\t\t\"$(DERIVED_FILE_DIR)/Pods-BarkTests-checkManifestLockResult.txt\",\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"diff \\\"${PODS_PODFILE_DIR_PATH}/Podfile.lock\\\" \\\"${PODS_ROOT}/Manifest.lock\\\" > /dev/null\\nif [ $? != 0 ] ; then\\n    # print error to STDERR\\n    echo \\\"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\\\" >&2\\n    exit 1\\nfi\\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\\necho \\\"SUCCESS\\\" > \\\"${SCRIPT_OUTPUT_FILE_0}\\\"\\n\";\n\t\t\tshowEnvVarsInLog = 0;\n\t\t};\n/* End PBXShellScriptBuildPhase section */\n\n/* Begin PBXSourcesBuildPhase section */\n\t\t0632CE1A20EC9098003FDF46 /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t19BE8EBF2D03514A009BF080 /* ImageDownloader.swift in Sources */,\n\t\t\t\t067AFB1C2E5D8BE300AE78E7 /* UNNotificationContent+Extension.swift in Sources */,\n\t\t\t\t0687F2AA2CCB7FA500B2A52F /* UIFont+Extension.swift in Sources */,\n\t\t\t\t0632CE2320EC9098003FDF46 /* NotificationViewController.swift in Sources */,\n\t\t\t\t069332222E6A8E3100F9387F /* String+Extension.swift in Sources */,\n\t\t\t\t06BCAE572CDB19420092867A /* GroupMuteSettingManager.swift in Sources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t0661A53B204FDA4100965E4E /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t0635A8072CE4883A0027E00F /* DonateCell.swift in Sources */,\n\t\t\t\t06840DBB272298FB001B3193 /* BKColor.swift in Sources */,\n\t\t\t\t06C2CF232685B88D0034B127 /* TextCell.swift in Sources */,\n\t\t\t\t06BBB896256518760076F63E /* NewServerViewModel.swift in Sources */,\n\t\t\t\t1EFB545D2C314A6800B8E51B /* BarkSplitViewController.swift in Sources */,\n\t\t\t\t06BBB8C92567B6730076F63E /* Operators.swift in Sources */,\n\t\t\t\t0603706920E1F89500F4CA05 /* PreviewCardCell.swift in Sources */,\n\t\t\t\t066890082D1946D500E106F2 /* MessageItemView.swift in Sources */,\n\t\t\t\t0627DABB298B6EA2002F3F69 /* DropBoxView.swift in Sources */,\n\t\t\t\t0672CB06256903F700570C9D /* MessageListViewModel.swift in Sources */,\n\t\t\t\t0627DABD2990D615002F3F69 /* BorderTextField.swift in Sources */,\n\t\t\t\t06E62C112D670F62004DC82B /* PushToCurrentIntent.swift in Sources */,\n\t\t\t\t0633E80A256A091B00ED0680 /* MJRefresh+Rx.swift in Sources */,\n\t\t\t\t0637FA8C20E0D7A700E80174 /* BaseViewController.swift in Sources */,\n\t\t\t\t0642B55C27EB149900453D91 /* MutableTextCellViewModel.swift in Sources */,\n\t\t\t\t062B98C8251B27AE004562E7 /* UINavigationItem+Extension.swift in Sources */,\n\t\t\t\t060481EE250F404500BC9799 /* SoundsViewController.swift in Sources */,\n\t\t\t\t06C777D02ECEFF210032A044 /* MarkdownParser.swift in Sources */,\n\t\t\t\t0699473D2D223094008D5E40 /* CustomTapTextView.swift in Sources */,\n\t\t\t\t06EEF333291CCFF400CA228A /* CryptoSettingController.swift in Sources */,\n\t\t\t\t0603706D20E23EC000F4CA05 /* BarkSFSafariViewController.swift in Sources */,\n\t\t\t\t06F08EA429B098DD006AB9CA /* CryptoSettingManager.swift in Sources */,\n\t\t\t\t0668900B2D19525400E106F2 /* ShowLessAndClearView.swift in Sources */,\n\t\t\t\t06BD4DAA2901352E003364DB /* Object+Dictionary.swift in Sources */,\n\t\t\t\t06C5953124811392006B98F3 /* ArchiveSettingCell.swift in Sources */,\n\t\t\t\t068EC15A27ED99E700D5D11E /* ServerListViewModel.swift in Sources */,\n\t\t\t\t06172FDC27F6DB06002333A4 /* ServerListTableViewCellViewModel.swift in Sources */,\n\t\t\t\t061894C729A75BEA00E001C2 /* Algorithm.swift in Sources */,\n\t\t\t\t066E0C8C2BB6AC9A00873838 /* AddSoundCell.swift in Sources */,\n\t\t\t\t065BE4502563D939002A8CA4 /* SoundCellViewModel.swift in Sources */,\n\t\t\t\t06F08EA729B1DDFE006AB9CA /* Error+Extension.swift in Sources */,\n\t\t\t\t0687F2A82CCB791A00B2A52F /* UIFont+Extension.swift in Sources */,\n\t\t\t\t066DF4822D2D31A60092B04E /* MessageDeleteTimeRange.swift in Sources */,\n\t\t\t\t06B1158F247BB1FB006D91FB /* Message.swift in Sources */,\n\t\t\t\t06BCAE562CDB19260092867A /* GroupMuteSettingManager.swift in Sources */,\n\t\t\t\t06172FDA27F6DAEF002333A4 /* ServerListTableViewCell.swift in Sources */,\n\t\t\t\t0653677829B727A60038BDB8 /* CryptoSettingRelay.swift in Sources */,\n\t\t\t\t06FB04042C53575400F3A213 /* SharedDefines.swift in Sources */,\n\t\t\t\t061894C529962EB900E001C2 /* GradientButton.swift in Sources */,\n\t\t\t\t06C595362481160F006B98F3 /* BKLabel.swift in Sources */,\n\t\t\t\t0637FA7820E0926D00E80174 /* BarkTargetType.swift in Sources */,\n\t\t\t\t064CABA6256BE9510018155C /* PreviewModel.swift in Sources */,\n\t\t\t\t0637FA7E20E0969800E80174 /* Client.swift in Sources */,\n\t\t\t\t0661A545204FDA4100965E4E /* HomeViewController.swift in Sources */,\n\t\t\t\t06BBB88725650C6C0076F63E /* ArchiveSettingManager.swift in Sources */,\n\t\t\t\t065BE44B2563D8E1002A8CA4 /* Reusable.swift in Sources */,\n\t\t\t\t0637FA8620E0AB6600E80174 /* UIColor+Extension.swift in Sources */,\n\t\t\t\t0637FA8A20E0D58800E80174 /* NewServerViewController.swift in Sources */,\n\t\t\t\t067B2EB525693E38008B6BE1 /* MessageSection.swift in Sources */,\n\t\t\t\t0637FA8220E09C4B00E80174 /* BarkNavigationController.swift in Sources */,\n\t\t\t\t0637FA7A20E092B300E80174 /* Observable+Extension.swift in Sources */,\n\t\t\t\t060481F0250F51CA00BC9799 /* SoundCell.swift in Sources */,\n\t\t\t\t0667D194247D1BA0005DE2ED /* Date+Extension.swift in Sources */,\n\t\t\t\t0604F7DF20620D4900B32F09 /* ServerManager.swift in Sources */,\n\t\t\t\t0667D192247D162C005DE2ED /* MessageTableViewCell.swift in Sources */,\n\t\t\t\t06E944762C07013000AC86AB /* RealmConfiguration.swift in Sources */,\n\t\t\t\t06EF49152D682A99008B91D2 /* PushResponse.swift in Sources */,\n\t\t\t\t0603706720E1E31600F4CA05 /* Defines.swift in Sources */,\n\t\t\t\t064CAB9E256BE9090018155C /* PreviewCardCellViewModel.swift in Sources */,\n\t\t\t\t065BE4552565055F002A8CA4 /* HomeViewModel.swift in Sources */,\n\t\t\t\t062B98C3251B2762004562E7 /* BKButton.swift in Sources */,\n\t\t\t\t06885EB6247FB9880004A303 /* MessageSettingsViewController.swift in Sources */,\n\t\t\t\t0635A8052CE47DFE0027E00F /* SettingSectionHeader.swift in Sources */,\n\t\t\t\t06F11E7727D9D5FB00F00298 /* QRScannerViewController.swift in Sources */,\n\t\t\t\t0608F06E2994D115006B8029 /* BKDropDownCell.swift in Sources */,\n\t\t\t\t06C5952D2480E3F8006B98F3 /* LabelCell.swift in Sources */,\n\t\t\t\t06787C392A710568008ABDD7 /* GesturePassTextView.swift in Sources */,\n\t\t\t\t068EC15827ED99C900D5D11E /* ServerListViewController.swift in Sources */,\n\t\t\t\t0637FA7C20E0930E00E80174 /* BarkApi.swift in Sources */,\n\t\t\t\t06BE84042E6EAE7100E6F856 /* InsetView.swift in Sources */,\n\t\t\t\t061E35862D1E5028009A2D6F /* MessageItemModel.swift in Sources */,\n\t\t\t\t06C2CF252685BDB80034B127 /* SpacerCell.swift in Sources */,\n\t\t\t\t06BBB8BC2567B3AD0076F63E /* ArchiveSettingCellViewModel.swift in Sources */,\n\t\t\t\t061C17082D1BDA4B00891D66 /* MessageGroupMoreView.swift in Sources */,\n\t\t\t\t0642B55A27EB13F100453D91 /* MutableTextCell.swift in Sources */,\n\t\t\t\t1EFB545F2C32514000B8E51B /* SectionViewModel-iPad.swift in Sources */,\n\t\t\t\t06EEF335291CD00000CA228A /* CryptoSettingViewModel.swift in Sources */,\n\t\t\t\t0637FA8020E0981E00E80174 /* BarkSettings.swift in Sources */,\n\t\t\t\t065BE4402563D649002A8CA4 /* SoundsViewModel.swift in Sources */,\n\t\t\t\t06EF49192D682B3B008B91D2 /* PushToOtherIntent.swift in Sources */,\n\t\t\t\t0647DB682CE604AA00102066 /* MessageSettingFooter.swift in Sources */,\n\t\t\t\t065BE4462563D7E5002A8CA4 /* ViewModelType.swift in Sources */,\n\t\t\t\t0603706B20E20A7C00F4CA05 /* String+Extension.swift in Sources */,\n\t\t\t\t068F66B3247BD84C00DAD25A /* MessageListViewController.swift in Sources */,\n\t\t\t\t0668900D2D19582400E106F2 /* MessageGroupHeaderView.swift in Sources */,\n\t\t\t\t1E73F99E2C282822002BF649 /* SectionViewController-iPad.swift in Sources */,\n\t\t\t\t0689CF4C2C7484A7007203A6 /* BarkTabBarController.swift in Sources */,\n\t\t\t\t06BBB8B72567AC140076F63E /* MessageSettingsViewModel.swift in Sources */,\n\t\t\t\t06BBB8C12567B3EF0076F63E /* BaseTableViewCell.swift in Sources */,\n\t\t\t\t0661A543204FDA4100965E4E /* AppDelegate.swift in Sources */,\n\t\t\t\t06EF49172D682AC4008B91D2 /* OptionsProvider.swift in Sources */,\n\t\t\t\t06F08EAF29B5D9FF006AB9CA /* HUD.swift in Sources */,\n\t\t\t\t06F808912EF4056C0007352A /* AppDelegate+Realm.swift in Sources */,\n\t\t\t\t065AE76B2987777F00323230 /* ArchiveSettingRelay.swift in Sources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t06CF783C21C7A50300A052D7 /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t069C6CA92ED03F07007244BB /* BKColor.swift in Sources */,\n\t\t\t\t06E944752C07012E00AC86AB /* RealmConfiguration.swift in Sources */,\n\t\t\t\t06E944732C06FF9200AC86AB /* ArchiveProcessor.swift in Sources */,\n\t\t\t\t06E9447A2C0704E500AC86AB /* ImageDownloader.swift in Sources */,\n\t\t\t\t06CF784C21C7A51200A052D7 /* NotificationService.swift in Sources */,\n\t\t\t\t06FB04052C53575400F3A213 /* SharedDefines.swift in Sources */,\n\t\t\t\t069C6CA42ED03C72007244BB /* MarkdownProcessor.swift in Sources */,\n\t\t\t\t067AFB1D2E5D8BED00AE78E7 /* UNNotificationContent+Extension.swift in Sources */,\n\t\t\t\t0653677629B719BC0038BDB8 /* CryptoSettingManager.swift in Sources */,\n\t\t\t\t06E9446F2C06FF1E00AC86AB /* BadgeProcessor.swift in Sources */,\n\t\t\t\t069C6CA52ED03E10007244BB /* MarkdownParser.swift in Sources */,\n\t\t\t\t06BCAE582CDB19420092867A /* GroupMuteSettingManager.swift in Sources */,\n\t\t\t\t06BCAE5B2CDB25120092867A /* MuteProcessor.swift in Sources */,\n\t\t\t\t06E9446D2C06FEC900AC86AB /* LevelProcessor.swift in Sources */,\n\t\t\t\t06E944682C06E40600AC86AB /* NotificationContentProcessor.swift in Sources */,\n\t\t\t\t06F08EA529B1DDA7006AB9CA /* Algorithm.swift in Sources */,\n\t\t\t\t06E944782C0701F300AC86AB /* IconProcessor.swift in Sources */,\n\t\t\t\t06BBB89125650CCF0076F63E /* ArchiveSettingManager.swift in Sources */,\n\t\t\t\t069332232E6A8E3100F9387F /* String+Extension.swift in Sources */,\n\t\t\t\t06D69E412C11983E00161A35 /* CallProcessor.swift in Sources */,\n\t\t\t\t06B11591247BC132006D91FB /* Message.swift in Sources */,\n\t\t\t\t06E9447C2C07052F00AC86AB /* ImageProcessor.swift in Sources */,\n\t\t\t\t06E9446A2C06E4A200AC86AB /* CiphertextProcessor.swift in Sources */,\n\t\t\t\t06E944712C06FF4C00AC86AB /* AutoCopyProcessor.swift in Sources */,\n\t\t\t\t0687F2A92CCB791A00B2A52F /* UIFont+Extension.swift in Sources */,\n\t\t\t\t06F08EA829B1DE0A006AB9CA /* Error+Extension.swift in Sources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t06EE1FCC26843E9300586708 /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t068A4B962D2E11CD00982449 /* MessageDeleteTimeRangeTest.swift in Sources */,\n\t\t\t\t06EE1FD326843E9300586708 /* BarkTests.swift in Sources */,\n\t\t\t\t063B909B272149BF00431EC2 /* HomeViewModelTests.swift in Sources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXSourcesBuildPhase section */\n\n/* Begin PBXTargetDependency section */\n\t\t0632CE2920EC9098003FDF46 /* PBXTargetDependency */ = {\n\t\t\tisa = PBXTargetDependency;\n\t\t\ttarget = 0632CE1D20EC9098003FDF46 /* NotificationContentExtension */;\n\t\t\ttargetProxy = 0632CE2820EC9098003FDF46 /* PBXContainerItemProxy */;\n\t\t};\n\t\t06CF784621C7A50300A052D7 /* PBXTargetDependency */ = {\n\t\t\tisa = PBXTargetDependency;\n\t\t\ttarget = 06CF783F21C7A50300A052D7 /* NotificationServiceExtension */;\n\t\t\ttargetProxy = 06CF784521C7A50300A052D7 /* PBXContainerItemProxy */;\n\t\t};\n\t\t06EE1FD626843E9300586708 /* PBXTargetDependency */ = {\n\t\t\tisa = PBXTargetDependency;\n\t\t\ttarget = 0661A53E204FDA4100965E4E /* Bark */;\n\t\t\ttargetProxy = 06EE1FD526843E9300586708 /* PBXContainerItemProxy */;\n\t\t};\n/* End PBXTargetDependency section */\n\n/* Begin PBXVariantGroup section */\n\t\t0632CE2420EC9098003FDF46 /* MainInterface.storyboard */ = {\n\t\t\tisa = PBXVariantGroup;\n\t\t\tchildren = (\n\t\t\t\t0632CE2520EC9098003FDF46 /* Base */,\n\t\t\t);\n\t\t\tname = MainInterface.storyboard;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t0661A54B204FDA4100965E4E /* LaunchScreen.storyboard */ = {\n\t\t\tisa = PBXVariantGroup;\n\t\t\tchildren = (\n\t\t\t\t0661A54C204FDA4100965E4E /* Base */,\n\t\t\t);\n\t\t\tname = LaunchScreen.storyboard;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n/* End PBXVariantGroup section */\n\n/* Begin XCBuildConfiguration section */\n\t\t0632CE2B20EC9098003FDF46 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = EF45D2197673129A8128ADB7 /* Pods-NotificationContentExtension.debug.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tCLANG_ENABLE_OBJC_WEAK = YES;\n\t\t\t\tCODE_SIGN_ENTITLEMENTS = NotificationContentExtension/NotificationContentExtension.entitlements;\n\t\t\t\tCODE_SIGN_IDENTITY = \"iPhone Developer\";\n\t\t\t\tCODE_SIGN_STYLE = Manual;\n\t\t\t\tCURRENT_PROJECT_VERSION = 1;\n\t\t\t\tINFOPLIST_FILE = NotificationContentExtension/Info.plist;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 13.0;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t\t\"@executable_path/../../Frameworks\",\n\t\t\t\t);\n\t\t\t\tMARKETING_VERSION = 1.0.0;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = me.fin.bark.NotificationContent;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tPROVISIONING_PROFILE_SPECIFIER = \"match Development me.fin.bark.NotificationContent\";\n\t\t\t\tSKIP_INSTALL = YES;\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTARGETED_DEVICE_FAMILY = 1;\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t0632CE2C20EC9098003FDF46 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 4A9C3B74281C8607B364CED7 /* Pods-NotificationContentExtension.release.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tCLANG_ENABLE_OBJC_WEAK = YES;\n\t\t\t\tCODE_SIGN_ENTITLEMENTS = NotificationContentExtension/NotificationContentExtension.entitlements;\n\t\t\t\tCODE_SIGN_IDENTITY = \"Apple Distribution\";\n\t\t\t\tCODE_SIGN_STYLE = Manual;\n\t\t\t\tCURRENT_PROJECT_VERSION = 1;\n\t\t\t\tINFOPLIST_FILE = NotificationContentExtension/Info.plist;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 13.0;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t\t\"@executable_path/../../Frameworks\",\n\t\t\t\t);\n\t\t\t\tMARKETING_VERSION = 1.0.0;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = me.fin.bark.NotificationContent;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tPROVISIONING_PROFILE_SPECIFIER = \"match AppStore me.fin.bark.NotificationContent\";\n\t\t\t\tSKIP_INSTALL = YES;\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTARGETED_DEVICE_FAMILY = 1;\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t0661A54F204FDA4100965E4E /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tCLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++14\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_COMMA = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_DOCUMENTATION_COMMENTS = YES;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_STRICT_PROTOTYPES = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;\n\t\t\t\tCLANG_WARN_UNREACHABLE_CODE = YES;\n\t\t\t\tCLANG_WARN__DUPLICATE_METHOD_MATCH = YES;\n\t\t\t\tCODE_SIGN_IDENTITY = \"iPhone Developer\";\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = dwarf;\n\t\t\t\tDEVELOPMENT_TEAM = 5U8LBRXG3A;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tENABLE_TESTABILITY = YES;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu11;\n\t\t\t\tGCC_DYNAMIC_NO_PIC = NO;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_OPTIMIZATION_LEVEL = 0;\n\t\t\t\tGCC_PREPROCESSOR_DEFINITIONS = (\n\t\t\t\t\t\"DEBUG=1\",\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t);\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNDECLARED_SELECTOR = YES;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 13.0;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = YES;\n\t\t\t\tONLY_ACTIVE_ARCH = YES;\n\t\t\t\tSDKROOT = iphoneos;\n\t\t\t\tSWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;\n\t\t\t\tSWIFT_EMIT_LOC_STRINGS = YES;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-Onone\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t0661A550204FDA4100965E4E /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tCLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++14\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_COMMA = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_DOCUMENTATION_COMMENTS = YES;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_STRICT_PROTOTYPES = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;\n\t\t\t\tCLANG_WARN_UNREACHABLE_CODE = YES;\n\t\t\t\tCLANG_WARN__DUPLICATE_METHOD_MATCH = YES;\n\t\t\t\tCODE_SIGN_IDENTITY = \"iPhone Developer\";\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = \"dwarf-with-dsym\";\n\t\t\t\tDEVELOPMENT_TEAM = 5U8LBRXG3A;\n\t\t\t\tENABLE_NS_ASSERTIONS = NO;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu11;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNDECLARED_SELECTOR = YES;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 13.0;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = NO;\n\t\t\t\tSDKROOT = iphoneos;\n\t\t\t\tSWIFT_COMPILATION_MODE = wholemodule;\n\t\t\t\tSWIFT_EMIT_LOC_STRINGS = YES;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-O\";\n\t\t\t\tVALIDATE_PRODUCT = YES;\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t0661A552204FDA4100965E4E /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 138CE8CB688587E893BC5C44 /* Pods-Bark.debug.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCODE_SIGN_ENTITLEMENTS = Bark/Bark.entitlements;\n\t\t\t\tCODE_SIGN_IDENTITY = \"iPhone Developer\";\n\t\t\t\tCODE_SIGN_STYLE = Manual;\n\t\t\t\tCURRENT_PROJECT_VERSION = 1;\n\t\t\t\tINFOPLIST_FILE = Bark/Info.plist;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 13.0;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tMARKETING_VERSION = 1.0.0;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = me.fin.bark;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tPROVISIONING_PROFILE_SPECIFIER = \"match Development me.fin.bark\";\n\t\t\t\tSUPPORTED_PLATFORMS = \"iphoneos iphonesimulator\";\n\t\t\t\tSUPPORTS_MACCATALYST = NO;\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t0661A553204FDA4100965E4E /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = A69B47DA6DB3B168D5770B45 /* Pods-Bark.release.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCODE_SIGN_ENTITLEMENTS = Bark/Bark.entitlements;\n\t\t\t\tCODE_SIGN_IDENTITY = \"Apple Distribution\";\n\t\t\t\tCODE_SIGN_STYLE = Manual;\n\t\t\t\tCURRENT_PROJECT_VERSION = 1;\n\t\t\t\tINFOPLIST_FILE = Bark/Info.plist;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 13.0;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tMARKETING_VERSION = 1.0.0;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = me.fin.bark;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tPROVISIONING_PROFILE_SPECIFIER = \"match AppStore me.fin.bark\";\n\t\t\t\tSUPPORTED_PLATFORMS = \"iphoneos iphonesimulator\";\n\t\t\t\tSUPPORTS_MACCATALYST = NO;\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t06CF784821C7A50300A052D7 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = F20815A821395CCA155806A4 /* Pods-NotificationServiceExtension.debug.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tCLANG_ENABLE_OBJC_WEAK = YES;\n\t\t\t\tCODE_SIGN_ENTITLEMENTS = NotificationServiceExtension/NotificationServiceExtension.entitlements;\n\t\t\t\tCODE_SIGN_IDENTITY = \"iPhone Developer\";\n\t\t\t\tCODE_SIGN_STYLE = Manual;\n\t\t\t\tCURRENT_PROJECT_VERSION = 1;\n\t\t\t\tINFOPLIST_FILE = NotificationServiceExtension/Info.plist;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 13.0;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t\t\"@executable_path/../../Frameworks\",\n\t\t\t\t);\n\t\t\t\tMARKETING_VERSION = 1.0.0;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;\n\t\t\t\tMTL_FAST_MATH = YES;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = me.fin.bark.NotificationServiceExtension;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tPROVISIONING_PROFILE_SPECIFIER = \"match Development me.fin.bark.NotificationServiceExtension\";\n\t\t\t\tSKIP_INSTALL = YES;\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t06CF784921C7A50300A052D7 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 519481D715B40109627E1B49 /* Pods-NotificationServiceExtension.release.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tCLANG_ENABLE_OBJC_WEAK = YES;\n\t\t\t\tCODE_SIGN_ENTITLEMENTS = NotificationServiceExtension/NotificationServiceExtension.entitlements;\n\t\t\t\tCODE_SIGN_IDENTITY = \"Apple Distribution\";\n\t\t\t\tCODE_SIGN_STYLE = Manual;\n\t\t\t\tCURRENT_PROJECT_VERSION = 1;\n\t\t\t\tINFOPLIST_FILE = NotificationServiceExtension/Info.plist;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 13.0;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t\t\"@executable_path/../../Frameworks\",\n\t\t\t\t);\n\t\t\t\tMARKETING_VERSION = 1.0.0;\n\t\t\t\tMTL_FAST_MATH = YES;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = me.fin.bark.NotificationServiceExtension;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tPROVISIONING_PROFILE_SPECIFIER = \"match AppStore me.fin.bark.NotificationServiceExtension\";\n\t\t\t\tSKIP_INSTALL = YES;\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t06EE1FD826843E9300586708 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = FB59D77AB30F7AD98BA72C3E /* Pods-BarkTests.debug.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tBUNDLE_LOADER = \"$(TEST_HOST)\";\n\t\t\t\tCLANG_ENABLE_OBJC_WEAK = YES;\n\t\t\t\tCODE_SIGN_IDENTITY = \"Apple Development\";\n\t\t\t\t\"CODE_SIGN_IDENTITY[sdk=macosx*]\" = \"Apple Development\";\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tINFOPLIST_FILE = BarkTests/Info.plist;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 13.0;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t\t\"@loader_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;\n\t\t\t\tMTL_FAST_MATH = YES;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = me.fin.BarkTests;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tPROVISIONING_PROFILE_SPECIFIER = \"\";\n\t\t\t\t\"PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]\" = \"\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t\tTEST_HOST = \"$(BUILT_PRODUCTS_DIR)/Bark.app/Bark\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t06EE1FD926843E9300586708 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 121D9B1ED4E8D26F345BC5C0 /* Pods-BarkTests.release.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tBUNDLE_LOADER = \"$(TEST_HOST)\";\n\t\t\t\tCLANG_ENABLE_OBJC_WEAK = YES;\n\t\t\t\tCODE_SIGN_IDENTITY = \"Apple Development\";\n\t\t\t\t\"CODE_SIGN_IDENTITY[sdk=macosx*]\" = \"Apple Development\";\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tINFOPLIST_FILE = BarkTests/Info.plist;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 13.0;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t\t\"@loader_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tMTL_FAST_MATH = YES;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = me.fin.BarkTests;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tPROVISIONING_PROFILE_SPECIFIER = \"\";\n\t\t\t\t\"PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]\" = \"\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t\tTEST_HOST = \"$(BUILT_PRODUCTS_DIR)/Bark.app/Bark\";\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n/* End XCBuildConfiguration section */\n\n/* Begin XCConfigurationList section */\n\t\t0632CE2D20EC9098003FDF46 /* Build configuration list for PBXNativeTarget \"NotificationContentExtension\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t0632CE2B20EC9098003FDF46 /* Debug */,\n\t\t\t\t0632CE2C20EC9098003FDF46 /* Release */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t0661A53A204FDA4100965E4E /* Build configuration list for PBXProject \"Bark\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t0661A54F204FDA4100965E4E /* Debug */,\n\t\t\t\t0661A550204FDA4100965E4E /* Release */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t0661A551204FDA4100965E4E /* Build configuration list for PBXNativeTarget \"Bark\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t0661A552204FDA4100965E4E /* Debug */,\n\t\t\t\t0661A553204FDA4100965E4E /* Release */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t06CF784A21C7A50300A052D7 /* Build configuration list for PBXNativeTarget \"NotificationServiceExtension\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t06CF784821C7A50300A052D7 /* Debug */,\n\t\t\t\t06CF784921C7A50300A052D7 /* Release */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t06EE1FD726843E9300586708 /* Build configuration list for PBXNativeTarget \"BarkTests\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t06EE1FD826843E9300586708 /* Debug */,\n\t\t\t\t06EE1FD926843E9300586708 /* Release */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n/* End XCConfigurationList section */\n\n/* Begin XCRemoteSwiftPackageReference section */\n\t\t06C777D12ECF05990032A044 /* XCRemoteSwiftPackageReference \"swift-markdown\" */ = {\n\t\t\tisa = XCRemoteSwiftPackageReference;\n\t\t\trepositoryURL = \"https://github.com/swiftlang/swift-markdown\";\n\t\t\trequirement = {\n\t\t\t\tkind = upToNextMajorVersion;\n\t\t\t\tminimumVersion = 0.7.3;\n\t\t\t};\n\t\t};\n/* End XCRemoteSwiftPackageReference section */\n\n/* Begin XCSwiftPackageProductDependency section */\n\t\t069C6CA62ED03E27007244BB /* Markdown */ = {\n\t\t\tisa = XCSwiftPackageProductDependency;\n\t\t\tpackage = 06C777D12ECF05990032A044 /* XCRemoteSwiftPackageReference \"swift-markdown\" */;\n\t\t\tproductName = Markdown;\n\t\t};\n\t\t06C777D22ECF05990032A044 /* Markdown */ = {\n\t\t\tisa = XCSwiftPackageProductDependency;\n\t\t\tpackage = 06C777D12ECF05990032A044 /* XCRemoteSwiftPackageReference \"swift-markdown\" */;\n\t\t\tproductName = Markdown;\n\t\t};\n/* End XCSwiftPackageProductDependency section */\n\t};\n\trootObject = 0661A537204FDA4100965E4E /* Project object */;\n}\n"
  },
  {
    "path": "Bark.xcodeproj/project.xcworkspace/contents.xcworkspacedata",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n   version = \"1.0\">\n   <FileRef\n      location = \"self:Bark.xcodeproj\">\n   </FileRef>\n</Workspace>\n"
  },
  {
    "path": "Bark.xcodeproj/xcshareddata/xcschemes/Bark.xcscheme",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n   LastUpgradeVersion = \"1640\"\n   version = \"1.3\">\n   <BuildAction\n      parallelizeBuildables = \"YES\"\n      buildImplicitDependencies = \"YES\">\n      <BuildActionEntries>\n         <BuildActionEntry\n            buildForTesting = \"YES\"\n            buildForRunning = \"YES\"\n            buildForProfiling = \"YES\"\n            buildForArchiving = \"YES\"\n            buildForAnalyzing = \"YES\">\n            <BuildableReference\n               BuildableIdentifier = \"primary\"\n               BlueprintIdentifier = \"0661A53E204FDA4100965E4E\"\n               BuildableName = \"Bark.app\"\n               BlueprintName = \"Bark\"\n               ReferencedContainer = \"container:Bark.xcodeproj\">\n            </BuildableReference>\n         </BuildActionEntry>\n      </BuildActionEntries>\n   </BuildAction>\n   <TestAction\n      buildConfiguration = \"Debug\"\n      selectedDebuggerIdentifier = \"Xcode.DebuggerFoundation.Debugger.LLDB\"\n      selectedLauncherIdentifier = \"Xcode.DebuggerFoundation.Launcher.LLDB\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\">\n      <Testables>\n         <TestableReference\n            skipped = \"NO\">\n            <BuildableReference\n               BuildableIdentifier = \"primary\"\n               BlueprintIdentifier = \"06EE1FCF26843E9300586708\"\n               BuildableName = \"BarkTests.xctest\"\n               BlueprintName = \"BarkTests\"\n               ReferencedContainer = \"container:Bark.xcodeproj\">\n            </BuildableReference>\n         </TestableReference>\n      </Testables>\n   </TestAction>\n   <LaunchAction\n      buildConfiguration = \"Debug\"\n      selectedDebuggerIdentifier = \"Xcode.DebuggerFoundation.Debugger.LLDB\"\n      selectedLauncherIdentifier = \"Xcode.DebuggerFoundation.Launcher.LLDB\"\n      launchStyle = \"0\"\n      useCustomWorkingDirectory = \"NO\"\n      ignoresPersistentStateOnLaunch = \"NO\"\n      debugDocumentVersioning = \"YES\"\n      debugServiceExtension = \"internal\"\n      allowLocationSimulation = \"YES\">\n      <BuildableProductRunnable\n         runnableDebuggingMode = \"0\">\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"0661A53E204FDA4100965E4E\"\n            BuildableName = \"Bark.app\"\n            BlueprintName = \"Bark\"\n            ReferencedContainer = \"container:Bark.xcodeproj\">\n         </BuildableReference>\n      </BuildableProductRunnable>\n   </LaunchAction>\n   <ProfileAction\n      buildConfiguration = \"Release\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\"\n      savedToolIdentifier = \"\"\n      useCustomWorkingDirectory = \"NO\"\n      debugDocumentVersioning = \"YES\">\n      <BuildableProductRunnable\n         runnableDebuggingMode = \"0\">\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"0661A53E204FDA4100965E4E\"\n            BuildableName = \"Bark.app\"\n            BlueprintName = \"Bark\"\n            ReferencedContainer = \"container:Bark.xcodeproj\">\n         </BuildableReference>\n      </BuildableProductRunnable>\n   </ProfileAction>\n   <AnalyzeAction\n      buildConfiguration = \"Debug\">\n   </AnalyzeAction>\n   <ArchiveAction\n      buildConfiguration = \"Release\"\n      revealArchiveInOrganizer = \"YES\">\n   </ArchiveAction>\n</Scheme>\n"
  },
  {
    "path": "Bark.xcodeproj/xcshareddata/xcschemes/NotificationContentExtension.xcscheme",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n   LastUpgradeVersion = \"1640\"\n   wasCreatedForAppExtension = \"YES\"\n   version = \"2.0\">\n   <BuildAction\n      parallelizeBuildables = \"YES\"\n      buildImplicitDependencies = \"YES\">\n      <BuildActionEntries>\n         <BuildActionEntry\n            buildForTesting = \"YES\"\n            buildForRunning = \"YES\"\n            buildForProfiling = \"YES\"\n            buildForArchiving = \"YES\"\n            buildForAnalyzing = \"YES\">\n            <BuildableReference\n               BuildableIdentifier = \"primary\"\n               BlueprintIdentifier = \"0632CE1D20EC9098003FDF46\"\n               BuildableName = \"NotificationContentExtension.appex\"\n               BlueprintName = \"NotificationContentExtension\"\n               ReferencedContainer = \"container:Bark.xcodeproj\">\n            </BuildableReference>\n         </BuildActionEntry>\n         <BuildActionEntry\n            buildForTesting = \"YES\"\n            buildForRunning = \"YES\"\n            buildForProfiling = \"YES\"\n            buildForArchiving = \"YES\"\n            buildForAnalyzing = \"YES\">\n            <BuildableReference\n               BuildableIdentifier = \"primary\"\n               BlueprintIdentifier = \"0661A53E204FDA4100965E4E\"\n               BuildableName = \"Bark.app\"\n               BlueprintName = \"Bark\"\n               ReferencedContainer = \"container:Bark.xcodeproj\">\n            </BuildableReference>\n         </BuildActionEntry>\n      </BuildActionEntries>\n   </BuildAction>\n   <TestAction\n      buildConfiguration = \"Debug\"\n      selectedDebuggerIdentifier = \"Xcode.DebuggerFoundation.Debugger.LLDB\"\n      selectedLauncherIdentifier = \"Xcode.DebuggerFoundation.Launcher.LLDB\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\">\n      <Testables>\n         <TestableReference\n            skipped = \"NO\">\n            <BuildableReference\n               BuildableIdentifier = \"primary\"\n               BlueprintIdentifier = \"06EE1FCF26843E9300586708\"\n               BuildableName = \"BarkTests.xctest\"\n               BlueprintName = \"BarkTests\"\n               ReferencedContainer = \"container:Bark.xcodeproj\">\n            </BuildableReference>\n         </TestableReference>\n      </Testables>\n   </TestAction>\n   <LaunchAction\n      buildConfiguration = \"Debug\"\n      selectedDebuggerIdentifier = \"\"\n      selectedLauncherIdentifier = \"Xcode.IDEFoundation.Launcher.PosixSpawn\"\n      launchStyle = \"0\"\n      askForAppToLaunch = \"Yes\"\n      useCustomWorkingDirectory = \"NO\"\n      ignoresPersistentStateOnLaunch = \"NO\"\n      debugDocumentVersioning = \"YES\"\n      debugServiceExtension = \"internal\"\n      allowLocationSimulation = \"YES\"\n      launchAutomaticallySubstyle = \"2\">\n      <BuildableProductRunnable\n         runnableDebuggingMode = \"0\">\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"0661A53E204FDA4100965E4E\"\n            BuildableName = \"Bark.app\"\n            BlueprintName = \"Bark\"\n            ReferencedContainer = \"container:Bark.xcodeproj\">\n         </BuildableReference>\n      </BuildableProductRunnable>\n   </LaunchAction>\n   <ProfileAction\n      buildConfiguration = \"Release\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\"\n      savedToolIdentifier = \"\"\n      useCustomWorkingDirectory = \"NO\"\n      debugDocumentVersioning = \"YES\"\n      askForAppToLaunch = \"Yes\"\n      launchAutomaticallySubstyle = \"2\">\n      <BuildableProductRunnable\n         runnableDebuggingMode = \"0\">\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"0661A53E204FDA4100965E4E\"\n            BuildableName = \"Bark.app\"\n            BlueprintName = \"Bark\"\n            ReferencedContainer = \"container:Bark.xcodeproj\">\n         </BuildableReference>\n      </BuildableProductRunnable>\n   </ProfileAction>\n   <AnalyzeAction\n      buildConfiguration = \"Debug\">\n   </AnalyzeAction>\n   <ArchiveAction\n      buildConfiguration = \"Release\"\n      revealArchiveInOrganizer = \"YES\">\n   </ArchiveAction>\n</Scheme>\n"
  },
  {
    "path": "Bark.xcodeproj/xcshareddata/xcschemes/NotificationServiceExtension.xcscheme",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n   LastUpgradeVersion = \"1640\"\n   wasCreatedForAppExtension = \"YES\"\n   version = \"2.0\">\n   <BuildAction\n      parallelizeBuildables = \"YES\"\n      buildImplicitDependencies = \"YES\">\n      <BuildActionEntries>\n         <BuildActionEntry\n            buildForTesting = \"YES\"\n            buildForRunning = \"YES\"\n            buildForProfiling = \"YES\"\n            buildForArchiving = \"YES\"\n            buildForAnalyzing = \"YES\">\n            <BuildableReference\n               BuildableIdentifier = \"primary\"\n               BlueprintIdentifier = \"06CF783F21C7A50300A052D7\"\n               BuildableName = \"NotificationServiceExtension.appex\"\n               BlueprintName = \"NotificationServiceExtension\"\n               ReferencedContainer = \"container:Bark.xcodeproj\">\n            </BuildableReference>\n         </BuildActionEntry>\n         <BuildActionEntry\n            buildForTesting = \"YES\"\n            buildForRunning = \"YES\"\n            buildForProfiling = \"YES\"\n            buildForArchiving = \"YES\"\n            buildForAnalyzing = \"YES\">\n            <BuildableReference\n               BuildableIdentifier = \"primary\"\n               BlueprintIdentifier = \"0661A53E204FDA4100965E4E\"\n               BuildableName = \"Bark.app\"\n               BlueprintName = \"Bark\"\n               ReferencedContainer = \"container:Bark.xcodeproj\">\n            </BuildableReference>\n         </BuildActionEntry>\n      </BuildActionEntries>\n   </BuildAction>\n   <TestAction\n      buildConfiguration = \"Debug\"\n      selectedDebuggerIdentifier = \"Xcode.DebuggerFoundation.Debugger.LLDB\"\n      selectedLauncherIdentifier = \"Xcode.DebuggerFoundation.Launcher.LLDB\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\">\n      <Testables>\n         <TestableReference\n            skipped = \"NO\">\n            <BuildableReference\n               BuildableIdentifier = \"primary\"\n               BlueprintIdentifier = \"06EE1FCF26843E9300586708\"\n               BuildableName = \"BarkTests.xctest\"\n               BlueprintName = \"BarkTests\"\n               ReferencedContainer = \"container:Bark.xcodeproj\">\n            </BuildableReference>\n         </TestableReference>\n      </Testables>\n   </TestAction>\n   <LaunchAction\n      buildConfiguration = \"Debug\"\n      selectedDebuggerIdentifier = \"\"\n      selectedLauncherIdentifier = \"Xcode.IDEFoundation.Launcher.PosixSpawn\"\n      launchStyle = \"0\"\n      askForAppToLaunch = \"Yes\"\n      useCustomWorkingDirectory = \"NO\"\n      ignoresPersistentStateOnLaunch = \"NO\"\n      debugDocumentVersioning = \"YES\"\n      debugServiceExtension = \"internal\"\n      allowLocationSimulation = \"YES\"\n      launchAutomaticallySubstyle = \"2\">\n      <BuildableProductRunnable\n         runnableDebuggingMode = \"0\">\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"0661A53E204FDA4100965E4E\"\n            BuildableName = \"Bark.app\"\n            BlueprintName = \"Bark\"\n            ReferencedContainer = \"container:Bark.xcodeproj\">\n         </BuildableReference>\n      </BuildableProductRunnable>\n   </LaunchAction>\n   <ProfileAction\n      buildConfiguration = \"Release\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\"\n      savedToolIdentifier = \"\"\n      useCustomWorkingDirectory = \"NO\"\n      debugDocumentVersioning = \"YES\"\n      askForAppToLaunch = \"Yes\"\n      launchAutomaticallySubstyle = \"2\">\n      <BuildableProductRunnable\n         runnableDebuggingMode = \"0\">\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"0661A53E204FDA4100965E4E\"\n            BuildableName = \"Bark.app\"\n            BlueprintName = \"Bark\"\n            ReferencedContainer = \"container:Bark.xcodeproj\">\n         </BuildableReference>\n      </BuildableProductRunnable>\n   </ProfileAction>\n   <AnalyzeAction\n      buildConfiguration = \"Debug\">\n   </AnalyzeAction>\n   <ArchiveAction\n      buildConfiguration = \"Release\"\n      revealArchiveInOrganizer = \"YES\">\n   </ArchiveAction>\n</Scheme>\n"
  },
  {
    "path": "Bark.xcworkspace/contents.xcworkspacedata",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n   version = \"1.0\">\n   <FileRef\n      location = \"group:Bark.xcodeproj\">\n   </FileRef>\n   <FileRef\n      location = \"group:Pods/Pods.xcodeproj\">\n   </FileRef>\n</Workspace>\n"
  },
  {
    "path": "Bark.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>IDEDidComputeMac32BitWarning</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "Bark.xcworkspace/xcshareddata/swiftpm/Package.resolved",
    "content": "{\n  \"originHash\" : \"bb354f5ee26f97a80bea96da70628d71c97b25e01e4e845afe4f5b1d0b6cf6e2\",\n  \"pins\" : [\n    {\n      \"identity\" : \"swift-cmark\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/swiftlang/swift-cmark.git\",\n      \"state\" : {\n        \"revision\" : \"5d9bdaa4228b381639fff09403e39a04926e2dbe\",\n        \"version\" : \"0.7.1\"\n      }\n    },\n    {\n      \"identity\" : \"swift-markdown\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/swiftlang/swift-markdown\",\n      \"state\" : {\n        \"revision\" : \"7d9a5ce307528578dfa777d505496bd5f544ad94\",\n        \"version\" : \"0.7.3\"\n      }\n    }\n  ],\n  \"version\" : 3\n}\n"
  },
  {
    "path": "BarkTests/BarkTests.swift",
    "content": "//\n//  BarkTests.swift\n//  BarkTests\n//\n//  Created by huangfeng on 2021/6/24.\n//  Copyright © 2021 Fin. All rights reserved.\n//\n\nimport XCTest\n\nclass BarkTests: XCTestCase {\n    override func setUpWithError() throws {\n        // Put setup code here. This method is called before the invocation of each test method in the class.\n    }\n\n    override func tearDownWithError() throws {\n        // Put teardown code here. This method is called after the invocation of each test method in the class.\n    }\n\n    func testExample() throws {\n        // This is an example of a functional test case.\n        // Use XCTAssert and related functions to verify your tests produce the correct results.\n    }\n\n    func testPerformanceExample() throws {\n        // This is an example of a performance test case.\n        measure {\n            // Put the code you want to measure the time of here.\n        }\n    }\n}\n"
  },
  {
    "path": "BarkTests/HomeViewModelTests.swift",
    "content": "//\n//  HomeViewModelTests.swift\n//  BarkTests\n//\n//  Created by huangfeng on 2021/10/21.\n//  Copyright © 2021 Fin. All rights reserved.\n//\n\n@testable import Bark\nimport RxCocoa\nimport RxSwift\nimport XCTest\n\nclass HomeViewModelTests: XCTestCase {\n\n    override func setUpWithError() throws {\n        // Put setup code here. This method is called before the invocation of each test method in the class.\n    }\n\n    override func tearDownWithError() throws {\n        // Put teardown code here. This method is called after the invocation of each test method in the class.\n    }\n\n    func testNewButtonClick() {\n        let exp = expectation(description: #function)\n        let homeViewModel = HomeViewModel()\n\n        let input = generateInput(addCustomServerTap: Driver.just(()))\n        let output = homeViewModel.transform(input: input)\n\n        output.push.drive { viewModel in\n            XCTAssertNotNil(viewModel as? NewServerViewModel)\n            exp.fulfill()\n        }.disposed(by: rx.disposeBag)\n\n        waitForExpectations(timeout: 1, handler: nil)\n    }\n\n    /// 测试 查看所有铃声 按钮点击\n    func testSoundsTap() {\n        let exp = expectation(description: #function)\n        let homeViewModel = HomeViewModel()\n\n        let input = generateInput()\n        let output = homeViewModel.transform(input: input)\n\n        // 测试是否能正常收到 push model\n        output.push.drive { viewModel in\n            XCTAssertTrue(viewModel is SoundsViewModel, \"Type Error\")\n            exp.fulfill()\n        }.disposed(by: rx.disposeBag)\n\n        // 发送点击事件\n        output.previews.drive { models in\n            guard let items = models.first?.items, let testPrevieModel = items.first(where: { model in\n                model.previewModel.moreViewModel is SoundsViewModel\n            }) else {\n                assertionFailure(\"Empty items\")\n                return\n            }\n            if let model = testPrevieModel.previewModel.moreViewModel {\n                testPrevieModel.noticeTap.accept(model)\n            }\n        }.disposed(by: rx.disposeBag)\n\n        waitForExpectations(timeout: 1, handler: nil)\n    }\n\n    func testCopy() {\n        let exp = expectation(description: #function)\n        let homeViewModel = HomeViewModel()\n\n        let input = generateInput()\n        let output = homeViewModel.transform(input: input)\n\n        let testStr = \"hello bark\"\n        // 测试是否正常 copy\n        output.copy.drive { str in\n            XCTAssertTrue(str == testStr)\n            exp.fulfill()\n        }.disposed(by: rx.disposeBag)\n\n        // 发送复制事件\n        output.previews.drive { models in\n            guard let items = models.first?.items, let testPrevieModel = items.first else {\n                assertionFailure(\"Empty items\")\n                return\n            }\n            testPrevieModel.copy.accept(testStr)\n        }.disposed(by: rx.disposeBag)\n        waitForExpectations(timeout: 1, handler: nil)\n    }\n\n    /// 测试推送状态 打开和关闭时，示例 tableView 和 startButton 是否正常显示和隐藏\n    func testAuthorizationStatus() {\n        let exp = expectation(description: #function)\n        let homeViewModel = HomeViewModel()\n\n        let notDeterminedInput = generateInput(authorizationStatus: Observable.just(UNAuthorizationStatus.notDetermined).asSingle())\n        let notDeterminedOutput = homeViewModel.transform(input: notDeterminedInput)\n\n        notDeterminedOutput.tableViewHidden.drive { hidden in\n            XCTAssertTrue(hidden == false)\n        }.disposed(by: rx.disposeBag)\n\n        let authorizedInput = generateInput(authorizationStatus: Observable.just(UNAuthorizationStatus.authorized).asSingle())\n        let authorizedOutput = homeViewModel.transform(input: authorizedInput)\n\n        authorizedOutput.tableViewHidden.drive { hidden in\n            XCTAssertTrue(hidden)\n            exp.fulfill()\n        }.disposed(by: rx.disposeBag)\n        waitForExpectations(timeout: 1, handler: nil)\n    }\n\n    /// 生成Input\n    private func generateInput(addCustomServerTap: Driver<Void> = Driver.empty(),\n                               serverListTap: Driver<Void> = Driver.empty(),\n                               viewDidAppear: Driver<Void> = Driver.empty(),\n                               start: Driver<Void> = Driver.empty(),\n                               clientState: Driver<Client.ClienState> = Driver.empty(),\n                               authorizationStatus: Single<UNAuthorizationStatus> = Observable.just(UNAuthorizationStatus.authorized).asSingle(),\n                               startRequestAuthorizationCreator: @escaping () -> Observable<Bool> = {\n                                   Observable.just(true)\n                               }) -> HomeViewModel.Input\n    {\n\n        return HomeViewModel.Input(\n            addCustomServerTap: addCustomServerTap,\n            serverListTap: serverListTap,\n            viewDidAppear: viewDidAppear,\n            start: start,\n            clientState: clientState,\n            authorizationStatus: authorizationStatus,\n            startRequestAuthorizationCreator: startRequestAuthorizationCreator\n        )\n    }\n}\n"
  },
  {
    "path": "BarkTests/Info.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>CFBundleDevelopmentRegion</key>\n\t<string>$(DEVELOPMENT_LANGUAGE)</string>\n\t<key>CFBundleExecutable</key>\n\t<string>$(EXECUTABLE_NAME)</string>\n\t<key>CFBundleIdentifier</key>\n\t<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>\n\t<key>CFBundleInfoDictionaryVersion</key>\n\t<string>6.0</string>\n\t<key>CFBundleName</key>\n\t<string>$(PRODUCT_NAME)</string>\n\t<key>CFBundlePackageType</key>\n\t<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>\n\t<key>CFBundleShortVersionString</key>\n\t<string>1.0</string>\n\t<key>CFBundleVersion</key>\n\t<string>1</string>\n</dict>\n</plist>\n"
  },
  {
    "path": "BarkTests/MessageDeleteTimeRangeTest.swift",
    "content": "//\n//  MessageDeleteTimeRangeTest.swift\n//  BarkTests\n//\n//  Created by huangfeng on 1/8/25.\n//  Copyright © 2025 Fin. All rights reserved.\n//\n@testable import Bark\nimport Testing\n\nstruct MessageDeleteTimeRangeTest {\n    @Test(\"检查时间范围区间是否正确\", arguments: [\n        MessageDeleteTimeRange.lastHour,\n        MessageDeleteTimeRange.today,\n        MessageDeleteTimeRange.todayAndYesterday,\n        MessageDeleteTimeRange.lastMonth,\n        MessageDeleteTimeRange.allTime,\n        MessageDeleteTimeRange.beforeOneHour,\n        MessageDeleteTimeRange.beforeToday,\n        MessageDeleteTimeRange.beforeYesterday,\n        MessageDeleteTimeRange.beforeOneMonth\n    ])\n    func testRange(range: MessageDeleteTimeRange) async throws {\n        let now = Date()\n        let lastHour = Calendar.current.date(byAdding: .hour, value: -1, to: now)!\n        let today = now.startOfDay\n        let yesterday = Calendar.current.date(byAdding: .day, value: -1, to: today)!.startOfDay\n        let lastMonth = Calendar.current.date(byAdding: .month, value: -1, to: now)!\n        \n        switch range {\n        case .lastHour:\n            let startDate = range.startDate\n            let endDate = range.endDate\n            #expect(startDate.timeInterval == lastHour.timeInterval && endDate.timeInterval == now.timeInterval)\n        case .today:\n            let startDate = range.startDate\n            let endDate = range.endDate\n            #expect(startDate.timeInterval == today.timeInterval && endDate.timeInterval == now.timeInterval)\n        case .todayAndYesterday:\n            let startDate = range.startDate\n            let endDate = range.endDate\n            #expect(startDate.timeInterval == yesterday.timeInterval && endDate.timeInterval == now.timeInterval)\n        case .lastMonth:\n            let startDate = range.startDate\n            let endDate = range.endDate\n            #expect(startDate.timeInterval == lastMonth.timeInterval && endDate.timeInterval == now.timeInterval)\n        case .allTime:\n            let startDate = range.startDate\n            let endDate = range.endDate\n            #expect(startDate.timeInterval == 0 && endDate.timeInterval == now.timeInterval)\n        case .beforeOneHour:\n            let startDate = range.startDate\n            let endDate = range.endDate\n            #expect(startDate.timeInterval == 0 && endDate.timeInterval == lastHour.timeInterval)\n        case .beforeToday:\n            let startDate = range.startDate\n            let endDate = range.endDate\n            #expect(startDate.timeInterval == 0 && endDate.timeInterval == today.timeInterval)\n        case .beforeYesterday:\n            let startDate = range.startDate\n            let endDate = range.endDate\n            #expect(startDate.timeInterval == 0 && endDate.timeInterval == yesterday.timeInterval)\n        case .beforeOneMonth:\n            let startDate = range.startDate\n            let endDate = range.endDate\n            #expect(startDate.timeInterval == 0 && endDate.timeInterval == lastMonth.timeInterval)\n        }\n    }\n}\n"
  },
  {
    "path": "CLAUDE.md",
    "content": "# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## Project Overview\n\nBark is an iOS push notification tool app that allows users to send custom push notifications to their devices via HTTP requests. It leverages Apple Push Notification service (APNs) and supports advanced iOS notification features like grouping, custom icons/sounds, time-sensitive notifications, critical alerts, and end-to-end encryption.\n\n## Development Commands\n\n### Dependencies\n```bash\n# Install CocoaPods dependencies\npod install\n\n# Always work with the workspace, not the project\n# Open: Bark.xcworkspace\n```\n\n### Testing\n```bash\n# Run tests via fastlane\nbundle exec fastlane tests\n\n# Run tests via xcodebuild\nxcodebuild test -workspace Bark.xcworkspace -scheme Bark\n```\n\n### Building\n```bash\n# Build the app\nxcodebuild -workspace Bark.xcworkspace -scheme Bark build\n\n# Build for TestFlight (requires proper certificates and environment variables)\nbundle exec fastlane beta\n```\n\n## Architecture\n\n### Core Components\n\n**Main App Target (Bark/)**\n- Entry point: `AppDelegate.swift` - handles app lifecycle, APNs registration, Realm setup\n- `AppDelegate+Realm.swift` - Realm database initialization and migration logic\n- Realm schema version: 17 (see `RealmConfiguration.swift`)\n\n**Notification Extensions**\n1. `NotificationServiceExtension/` - UNNotificationServiceExtension that processes incoming notifications\n   - `NotificationService.swift` - orchestrates notification processing through a pipeline of processors\n   - `Processor/` directory contains specialized processors that run sequentially:\n     - `CiphertextProcessor` - decrypts encrypted push content (must run first)\n     - `MarkdownProcessor` - renders markdown in notification body\n     - `LevelProcessor` - handles notification levels (active/timeSensitive/passive/critical)\n     - `BadgeProcessor` - manages app badge count\n     - `AutoCopyProcessor` - auto-copies content to clipboard\n     - `ArchiveProcessor` - saves message to Realm database\n     - `MuteProcessor` - handles group muting\n     - `CallProcessor` - plays repeating sound for 30 seconds\n     - `ImageProcessor` - downloads and attaches images\n     - `IconProcessor` - sets custom notification icon (iOS 15+, runs last as it may timeout)\n\n2. `notificationContentExtension/` - custom notification UI\n   - `NotificationViewController.swift` - displays custom content in notification\n\n### Project Structure\n\n**Model/** - Data models\n- `Message.swift` - Realm object for stored notifications (properties: title, subtitle, body, bodyType, url, image, group, createDate)\n- `MessageItemModel.swift` - view model for message display\n- `Algorithm.swift` - encryption/decryption algorithms for end-to-end encryption\n\n**Controller/** - View controllers using MVVM pattern\n- Suffix `ViewController` for views, `ViewModel` for view models\n- Key screens:\n  - `HomeViewController` + `HomeViewModel` - main screen showing tutorial and test URL\n  - `MessageListViewController` + `MessageListViewModel` - notification history grouped by date/group\n  - `ServerListViewController` + `ServerListViewModel` - manage multiple push servers\n  - `CryptoSettingController` + `CryptoSettingViewModel` - configure encryption keys\n  - `SoundsViewController` + `SoundsViewModel` - manage custom notification sounds\n\n**View/** - Reusable UI components\n- Custom cells, buttons, text views\n- `View/MessageList/` - specialized views for message display with markdown support\n\n**Common/** - Shared utilities and managers\n- `Client.swift` - singleton managing app state, device token, current tab navigation\n- `ServerManager.swift` - manages multiple push servers (default: https://api.day.app)\n- `BarkSettings.swift` - UserDefaults wrapper for persistent settings using DefaultsKit\n- `RealmConfiguration.swift` - shared Realm configuration between app and extensions\n- `Moya/` - Network layer using Moya + RxSwift\n  - `BarkApi.swift` - API endpoints (ping, register)\n  - `BarkTargetType.swift` - base target type\n  - `Observable+Extension.swift` - Rx helpers\n\n### Key Technologies\n\n- **RxSwift/RxCocoa** - Reactive programming for data binding and async operations\n- **Realm** - Local database for message persistence (shared between app and extensions via App Groups)\n- **Moya** - Network abstraction layer over Alamofire\n- **Material/SnapKit** - UI framework and Auto Layout DSL\n- **CryptoSwift** - Encryption for secure push notifications\n- **Kingfisher** - Image downloading and caching\n\n### Data Flow\n\n1. **Device Registration**: App requests APNs token → sends to server via `/register` endpoint → server stores device token with user's key\n2. **Receiving Push**: Server sends push → APNs delivers to device → `NotificationServiceExtension` processes through processor pipeline → notification displayed\n3. **Message Storage**: `ArchiveProcessor` saves to Realm → Darwin notification sent to main app → main app refreshes message list\n4. **Encryption Flow**: If ciphertext parameter present → `CiphertextProcessor` decrypts using locally stored key → continues normal processing\n\n### App Groups & Data Sharing\n\nThe app uses App Groups to share data between the main app and notification extensions:\n- Realm database at shared Documents directory\n- Settings via `BarkSettings` (DefaultsKit)\n- Darwin notifications for cross-process communication (e.g., \"com.bark.newmessage\")\n\n### Server Architecture\n\nUsers can configure multiple push servers (`ServerManager`):\n- Default server: `https://api.day.app`\n- Each server has: id, address, key, state, optional name\n- Old single-server data (pre-v1.2.6) is automatically migrated to new multi-server format\n\n### Notification Processing Pipeline\n\nOrder matters - processors run sequentially in `NotificationService.swift`:\n1. Ciphertext decryption (must be first - may contain all push data)\n2. Content processing (markdown, level, badge, autoCopy)\n3. Database archiving\n4. Mute checking\n5. Sound effects (call mode)\n6. Media attachments (image, icon - icon last as it may timeout)\n\n## Important Notes\n\n- Always open `Bark.xcworkspace`, never `Bark.xcproject`\n- Minimum iOS deployment target: 13.0\n- Realm schema migrations are handled in `RealmConfiguration.swift` - increment `schemaVersion` when changing Message model\n- The app supports both iPhone and iPad (with split view controller on iPad)\n- Localization is managed via `Localizable.xcstrings`\n- Custom notification sounds are in `Sounds/` directory\n- The app uses Material Design components for UI\n- Code signing is disabled for Pods (see Podfile post_install)\n\n## Testing\n\nTests are located in `BarkTests/` directory. The CI runs tests on every push to master via GitHub Actions (`.github/workflows/tests.yaml`).\n"
  },
  {
    "path": "Common/ArchiveSettingManager.swift",
    "content": "//\n//  ArchiveSettingManager.swift\n//  Bark\n//\n//  Created by huangfeng on 2020/5/29.\n//  Copyright © 2020 Fin. All rights reserved.\n//\n\nimport UIKit\n\nclass ArchiveSettingManager: NSObject {\n    static let shared = ArchiveSettingManager()\n    let defaults = UserDefaults(suiteName: \"group.bark\")\n    var isArchive: Bool {\n        get {\n            return defaults?.value(forKey: \"isArchive\") as? Bool ?? true\n        }\n        set {\n            defaults?.set(newValue, forKey: \"isArchive\")\n        }\n    }\n\n    override private init() {\n        super.init()\n    }\n}\n"
  },
  {
    "path": "Common/ArchiveSettingRelay.swift",
    "content": "//\n//  ArchiveSettingRelay.swift\n//  Bark\n//\n//  Created by huangfeng on 2023/1/30.\n//  Copyright © 2023 Fin. All rights reserved.\n//\n\nimport RxCocoa\nimport UIKit\nclass ArchiveSettingRelay: NSObject {\n    static let shared = ArchiveSettingRelay()\n    let isArchiveRelay: BehaviorRelay<Bool>\n\n    override private init() {\n        self.isArchiveRelay = BehaviorRelay<Bool>(value: ArchiveSettingManager.shared.isArchive)\n        super.init()\n\n        self.isArchiveRelay.subscribe { val in\n            ArchiveSettingManager.shared.isArchive = val\n        }.disposed(by: rx.disposeBag)\n    }\n}\n"
  },
  {
    "path": "Common/BKColor.swift",
    "content": "//\n//  BKColor.swift\n//  Bark\n//\n//  Created by huangfeng on 2021/10/22.\n//  Copyright © 2021 Fin. All rights reserved.\n//\n\nimport UIKit\n\nclass BKColor: NSObject {\n\n    enum grey {\n        public static let base = UIColor(named: \"grey_base\")!\n        public static let darken1 = UIColor(named: \"grey_darken1\")!\n        public static let darken2 = UIColor(named: \"grey_darken2\")!\n        public static let darken3 = UIColor(named: \"grey_darken3\")!\n        public static let darken4 = UIColor(named: \"grey_darken4\")!\n        public static let lighten1 = UIColor(named: \"grey_lighten1\")!\n        public static let lighten2 = UIColor(named: \"grey_lighten2\")!\n        public static let lighten3 = UIColor(named: \"grey_lighten3\")!\n        public static let lighten4 = UIColor(named: \"grey_lighten4\")!\n        public static let lighten5 = UIColor(named: \"grey_lighten5\")!\n    }\n\n    enum blue {\n        public static let base = UIColor(named: \"blue_base\")!\n        public static let darken1 = UIColor(named: \"blue_darken1\")!\n        public static let darken5 = UIColor(named: \"blue_darken5\")!\n    }\n\n    enum lightBlue {\n        public static let darken3 = UIColor(named: \"lightBlue_darken3\")!\n    }\n\n    public static let white = UIColor(named: \"white\")!\n    \n    public static let black = UIColor(named: \"black\")!\n\n    enum background {\n        public static let primary = UIColor(named: \"background\")!\n        public static let secondary = UIColor(named: \"background_seconday\")!\n    }\n}\n"
  },
  {
    "path": "Common/BarkSettings.swift",
    "content": "//\n//  BarkSettings.swift\n//  Bark\n//\n//  Created by huangfeng on 2018/6/25.\n//  Copyright © 2018 Fin. All rights reserved.\n//\n\nimport DefaultsKit\nimport UIKit\n\nenum BarkSettingKey: String {\n    /// 存放key , 1.2.6 版本`之后`不再使用\n    case key = \"me.fin.bark.key\"\n    case servers = \"me.fin.bark.servers\"\n    \n    /// 1.2.6 版本`之前`保存当前 server 的 key，不再使用\n    case currentServer = \"me.fin.bark.servers.current\"\n    /// 1.2.6 版本`之后`用于保存 server 的 id\n    case currentServerId = \"me.fin.bark.servers.currentServerId\"\n    \n    case selectedViewControllerIndex = \"me.fin.bark.selectedViewControllerIndex\"\n}\n\nclass BarkSettings {\n    static let shared = BarkSettings()\n    private init() {}\n    \n    subscript(key: String) -> String? {\n        get {\n            let storeKey = Key<String>(key)\n            return Defaults.shared.get(for: storeKey)\n        }\n        set {\n            let storeKey = Key<String>(key)\n            if let value = newValue {\n                Defaults.shared.set(value, for: storeKey)\n            }\n            else {\n                Defaults.shared.clear(storeKey)\n            }\n        }\n    }\n    \n    subscript(key: BarkSettingKey) -> String? {\n        get {\n            return self[key.rawValue]\n        }\n        set {\n            self[key.rawValue] = newValue\n        }\n    }\n    \n    subscript<T: Codable>(key: String) -> T? {\n        get {\n            let storeKey = Key<T>(key)\n            return Defaults.shared.get(for: storeKey)\n        }\n        set {\n            let storeKey = Key<T>(key)\n            if let value = newValue {\n                Defaults.shared.set(value, for: storeKey)\n            }\n            else {\n                Defaults.shared.clear(storeKey)\n            }\n        }\n    }\n\n    subscript<T: Codable>(key: BarkSettingKey) -> T? {\n        get {\n            return self[key.rawValue]\n        }\n        set {\n            self[key.rawValue] = newValue\n        }\n    }\n}\n\nlet Settings = BarkSettings.shared\n"
  },
  {
    "path": "Common/Client.swift",
    "content": "//\n//  Client.swift\n//  Bark\n//\n//  Created by huangfeng on 2018/6/25.\n//  Copyright © 2018 Fin. All rights reserved.\n//\n\nimport RxCocoa\nimport RxSwift\nimport UIKit\nimport UserNotifications\n\nclass Client: NSObject {\n    static let shared = Client()\n    override private init() {\n        super.init()\n    }\n\n    var window: UIWindow? {\n        return UIApplication.shared.delegate?.window ?? nil\n    }\n\n    var currentSnackbarController: BarkSnackbarController? {\n        return self.window?.rootViewController as? BarkSnackbarController\n    }\n\n    var currentTabBarController: StateRestoringTabBarContr? {\n        guard let snackbarController = self.currentSnackbarController else {\n            return nil\n        }\n        if #available(iOS 14, *), UIDevice.current.userInterfaceIdiom == .pad {\n            return (snackbarController.rootViewController as? BarkSplitViewController)?.compactController\n        } else {\n            return snackbarController.rootViewController as? BarkTabBarController\n        }\n    }\n\n    var currentTabPage: TabPage {\n        get {\n            guard let tabBarController = self.currentTabBarController else {\n                return .unknown\n            }\n            return TabPage(rawValue: tabBarController.selectedIndex) ?? .unknown\n        }\n        set {\n            guard let tabBarController = self.currentTabBarController else {\n                return\n            }\n            tabBarController.currentSelectedIndex = newValue.rawValue\n        }\n    }\n    \n    enum ClienState {\n        case ok\n        case unRegister\n        case serverError(error: ApiError)\n        static func == (lhs: ClienState, rhs: ClienState) -> Bool {\n            switch (lhs, rhs) {\n            case (.ok, .ok):\n                return true\n            case (.unRegister, .unRegister):\n                return true\n            case (.serverError(let error1), .serverError(let error2)):\n                return error1.localizedDescription == error2.localizedDescription\n            default:\n                return false\n            }\n        }\n    }\n\n    var deviceToken = BehaviorRelay<String?>(value: nil)\n    var state = BehaviorRelay<ClienState>(value: .ok)\n    \n    func registerForRemoteNotifications() {\n        let center = UNUserNotificationCenter.current()\n        center.requestAuthorization(options: [.alert, .sound, .badge, .criticalAlert], completionHandler: { (_ granted: Bool, _: Error?) in\n            if granted {\n                dispatch_sync_safely_main_queue {\n                    UIApplication.shared.registerForRemoteNotifications()\n                }\n            } else {\n                print(\"没有打开推送\")\n            }\n        })\n    }\n    \n    func openUrl(url: URL) {\n        if [\"http\", \"https\"].contains(url.scheme?.lowercased() ?? \"\") {\n            UIApplication.shared.open(url, options: [UIApplication.OpenExternalURLOptionsKey.universalLinksOnly: true]) { success in\n                if !success {\n                    // 打不开Universal Link时，则用 safari 打开\n                    UIApplication.shared.open(url, options: [:], completionHandler: nil)\n                }\n            }\n        } else {\n            UIApplication.shared.open(url, options: [:], completionHandler: nil)\n        }\n    }\n}\n"
  },
  {
    "path": "Common/CryptoSettingManager.swift",
    "content": "//\n//  CryptoSettingManager.swift\n//  Bark\n//\n//  Created by huangfeng on 2023/3/2.\n//  Copyright © 2023 Fin. All rights reserved.\n//\n\nimport Foundation\n\nclass CryptoSettingManager: NSObject {\n    static let shared = CryptoSettingManager()\n    let defaults = UserDefaults(suiteName: \"group.bark\")\n    var fields: CryptoSettingFields? {\n        get {\n            guard let data:Data =  defaults?.value(forKey: \"cryptoSettingFields\") as? Data else {\n                return nil\n            }\n            guard let fields = try? JSONDecoder().decode(CryptoSettingFields.self, from: data) else {\n                return nil\n            }\n            return fields\n        }\n        set {\n            guard let newValue = newValue else {\n                defaults?.removeObject(forKey: \"cryptoSettingFields\")\n                return\n            }\n            guard let encoded = try? JSONEncoder().encode(newValue) else{\n                return\n            }\n            defaults?.set(encoded, forKey: \"cryptoSettingFields\")\n        }\n    }\n\n    override private init() {\n        super.init()\n    }\n}\n\n"
  },
  {
    "path": "Common/CryptoSettingRelay.swift",
    "content": "//\n//  CryptoSettingRelay.swift\n//  Bark\n//\n//  Created by huangfeng on 2023/3/7.\n//  Copyright © 2023 Fin. All rights reserved.\n//\n\nimport Foundation\nimport RxCocoa\n\nclass CryptoSettingRelay: NSObject {\n    static let shared = CryptoSettingRelay()\n    let fields: BehaviorRelay<CryptoSettingFields?>\n\n    override private init() {\n        self.fields = BehaviorRelay<CryptoSettingFields?>(value: CryptoSettingManager.shared.fields)\n        super.init()\n\n        self.fields.subscribe { val in\n            CryptoSettingManager.shared.fields = val\n        }.disposed(by: rx.disposeBag)\n    }\n}\n"
  },
  {
    "path": "Common/Date+Extension.swift",
    "content": "//\n//  Date+Extension.swift\n//  Bark\n//\n//  Created by huangfeng on 2020/5/26.\n//  Copyright © 2020 Fin. All rights reserved.\n//\n\nimport UIKit\n\nextension Date {\n    func formatString(format: String) -> String {\n        let formatter = DateFormatter()\n        formatter.dateFormat = format\n        return formatter.string(for: self) ?? \"\"\n    }\n\n    func agoFormatString() -> String {\n        let clendar = NSCalendar(calendarIdentifier: .gregorian)\n        let cps = clendar?.components([.hour, .minute, .second, .day, .month, .year], from: self, to: Date(), options: .wrapComponents)\n\n        let year = cps!.year!\n        let month = cps!.month!\n        let day = cps!.day!\n        let hour = cps!.hour!\n        let minute = cps!.minute!\n\n        if year > 0 || month > 0 || day > 0 || hour > 12 {\n            return formatString(format: \"yyyy-MM-dd HH:mm\")\n        }\n        if hour > 1 {\n            return formatString(format: \"HH:mm\")\n        }\n        if hour > 0 {\n            if minute > 0 {\n                return \"timeMinHourAgo\".localized(with: hour, minute)\n            }\n            return \"timeHourAgo\".localized(with: hour)\n        }\n        if minute > 1 {\n            return \"timeMinAgo\".localized(with: minute)\n        }\n        return \"timeJustNow\".localized\n    }\n}\n\nextension Date {\n    var month: Int {\n        return Calendar.current.component(.month, from: self)\n    }\n\n    var timeInterval: Int {\n        return Int(timeIntervalSince1970)\n    }\n    \n    var isLastDayOfMonth: Bool {\n        return dayAfter.month != month\n    }\n}\n\nextension Date {\n    static var yesterday: Date { return Date().dayBefore }\n    static var tomorrow: Date { return Date().dayAfter }\n    static var lastHour: Date { return Calendar.current.date(byAdding: .hour, value: -1, to: Date())! }\n    static var lastMonth: Date { return Calendar.current.date(byAdding: .month, value: -1, to: Date())! }\n    \n    var dayBefore: Date {\n        return Calendar.current.date(byAdding: .day, value: -1, to: startOfDay)!\n    }\n\n    var dayAfter: Date {\n        return Calendar.current.date(byAdding: .day, value: 1, to: startOfDay)!\n    }\n\n    var startOfDay: Date {\n        return Calendar.current.date(bySettingHour: 0, minute: 0, second: 0, of: self)!\n    }\n\n    var endOfDay: Date {\n        return Calendar.current.date(bySettingHour: 23, minute: 59, second: 59, of: self)!\n    }\n}\n"
  },
  {
    "path": "Common/Defines.swift",
    "content": "//\n//  Defines.swift\n//  Bark\n//\n//  Created by huangfeng on 2018/6/26.\n//  Copyright © 2018 Fin. All rights reserved.\n//\n\nimport RxCocoa\nimport UIKit\n\n/// 将代码安全的运行在主线程\nfunc dispatch_sync_safely_main_queue(_ block: () -> ()) {\n    if Thread.isMainThread {\n        block()\n    } else {\n        DispatchQueue.main.sync {\n            block()\n        }\n    }\n}\n\nextension UIViewController {\n    func showSnackbar(text: String) {\n        self.snackbarController?.snackbar.text = text\n        self.snackbarController?.animate(snackbar: .visible)\n        self.snackbarController?.animate(snackbar: .hidden, delay: 3)\n    }\n}\n\nlet kNavigationHeight: CGFloat = {\n    kSafeAreaInsets.top + 44\n}()\n\nlet kSafeAreaInsets: UIEdgeInsets = {\n    UIWindow().safeAreaInsets\n}()\n\nfunc castOrThrow<T>(_ resultType: T.Type, _ object: Any) throws -> T {\n    guard let returnValue = object as? T else {\n        throw RxCocoaError.castingError(object: object, targetType: resultType)\n    }\n\n    return returnValue\n}\n"
  },
  {
    "path": "Common/Error+Extension.swift",
    "content": "//\n//  Error+Extension.swift\n//  Bark\n//\n//  Created by huangfeng on 2023/3/3.\n//  Copyright © 2023 Fin. All rights reserved.\n//\n\nimport Foundation\n\nextension String: @retroactive Error {}\n\npublic enum ApiError: Swift.Error {\n    case Error(info: String)\n    case AccountBanned(info: String)\n}\n\nextension Swift.Error {\n    func rawString() -> String {\n        if let err = self as? String {\n            return err\n        }\n        guard let err = self as? ApiError else {\n            return self.localizedDescription\n        }\n        switch err {\n        case .Error(let info):\n            return info\n        case .AccountBanned(let info):\n            return info\n        }\n    }\n}\n"
  },
  {
    "path": "Common/GroupMuteSettingManager.swift",
    "content": "//\n//  GroupMuteSettingManager.swift\n//  Bark\n//\n//  Created by huangfeng on 11/6/24.\n//  Copyright © 2024 Fin. All rights reserved.\n//\n\nimport UIKit\n\nlet kGroupMuteSettingKey = \"groupMuteSettings\"\n\n/// 保存各分组的静音截止时间，注意 NotificationServiceExtension 和 NotificationContentExtension 是不同进程，不共享单例的（别用单例）\nclass GroupMuteSettingManager: NSObject {\n    let defaults = UserDefaults(suiteName: \"group.bark\")\n    \n    var settings: [String: Date] = [:] {\n        didSet {\n            defaults?.set(settings, forKey: kGroupMuteSettingKey)\n        }\n    }\n\n    override init() {\n        super.init()\n        if let settings = defaults?.dictionary(forKey: kGroupMuteSettingKey) as? [String: Date] {\n            self.settings = settings\n        }\n        // 清理过期的设置\n        for setting in settings {\n            if setting.value < Date() {\n                self.settings.removeValue(forKey: setting.key)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Common/MJRefresh+Rx.swift",
    "content": "//\n//  MJRefresh+Rx.swift\n//  Bark\n//\n//  Created by huangfeng on 2020/11/22.\n//  Copyright © 2020 Fin. All rights reserved.\n//\n\nimport Foundation\nimport MJRefresh\nimport RxCocoa\nimport RxSwift\n\nextension Reactive where Base: MJRefreshComponent {\n    var refresh: ControlEvent<Void> {\n        let source = Observable<Void>.create { [weak control = self.base] observer -> Disposable in\n            MainScheduler.ensureExecutingOnScheduler()\n            guard let control = control else {\n                observer.onCompleted()\n                return Disposables.create()\n            }\n            control.refreshingBlock = {\n                observer.onNext(())\n            }\n            return Disposables.create()\n        }\n        return ControlEvent(events: source)\n    }\n}\n\nenum MJRefreshAction {\n    /// 不做任何事情\n    case none\n    /// 开始刷新\n    case begainRefresh\n    /// 停止刷新\n    case endRefresh\n    /// 开始加载更多\n    case begainLoadmore\n    /// 停止加载更多\n    case endLoadmore\n    /// 显示无更多数据\n    case showNomoreData\n    /// 重置无更多数据\n    case resetNomoreData\n}\n\nextension Reactive where Base: UIScrollView {\n    /// 执行的操作类型\n    var refreshAction: Binder<MJRefreshAction> {\n        return Binder(base) { target, action in\n\n            switch action {\n            case .begainRefresh:\n                // 下拉刷新使用 UIRefreshControl\n                if let control = target.refreshControl {\n                    control.beginRefreshing()\n                }\n            case .endRefresh:\n                if let control = target.refreshControl {\n                    control.endRefreshing()\n                }\n            case .begainLoadmore:\n                if let footer = target.mj_footer {\n                    footer.beginRefreshing()\n                }\n            case .endLoadmore:\n                if let footer = target.mj_footer {\n                    footer.endRefreshing()\n                }\n            case .showNomoreData:\n                if let footer = target.mj_footer {\n                    footer.endRefreshingWithNoMoreData()\n                }\n            case .resetNomoreData:\n                if let footer = target.mj_footer {\n                    footer.resetNoMoreData()\n                }\n            case .none:\n                break\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Common/MarkdownParser.swift",
    "content": "//\n//  MarkdownParser.swift\n//  Bark\n//\n//  Created by huangfeng on 2025/11/20.\n//  Copyright © 2025 Fin. All rights reserved.\n//\n\nimport Markdown\nimport UIKit\n\nclass MarkdownParser {\n    struct Configuration {\n        let baseFont: UIFont\n        let baseColor: UIColor\n        let linkColor: UIColor\n        let codeTextColor: UIColor\n        let codeBackgroundColor: UIColor\n        let codeBlockTextColor: UIColor\n        let quoteColor: UIColor\n        \n        init(\n            baseFont: UIFont = UIFont.preferredFont(ofSize: 14),\n            baseColor: UIColor = BKColor.grey.darken4,\n            linkColor: UIColor = BKColor.blue.base,\n            codeTextColor: UIColor = BKColor.blue.base,\n            codeBackgroundColor: UIColor = BKColor.grey.lighten4,\n            codeBlockTextColor: UIColor = BKColor.grey.darken4,\n            quoteColor: UIColor = BKColor.grey.base\n        ) {\n            self.baseFont = baseFont\n            self.baseColor = baseColor\n            self.linkColor = linkColor\n            self.codeTextColor = codeTextColor\n            self.codeBackgroundColor = codeBackgroundColor\n            self.codeBlockTextColor = codeBlockTextColor\n            self.quoteColor = quoteColor\n        }\n    }\n    \n    private let configuration: Configuration\n    \n    init(configuration: Configuration = Configuration()) {\n        self.configuration = configuration\n    }\n    \n    func parse(_ markdown: String) -> NSAttributedString {\n        let document = Document(parsing: markdown)\n        var walker = AttributedStringWalker(configuration: configuration)\n        walker.visit(document)\n        return walker.attributedString\n    }\n}\n\nprivate struct AttributedStringWalker: MarkupWalker {\n    let configuration: MarkdownParser.Configuration\n    let attributedString = NSMutableAttributedString()\n    \n    var attributes: [NSAttributedString.Key: Any] = [:]\n    \n    enum ListType {\n        case ordered\n        case unordered\n    }\n    var listStack: [(type: ListType, index: Int)] = []\n    \n    init(configuration: MarkdownParser.Configuration) {\n        self.configuration = configuration\n        self.attributes = [\n            .font: configuration.baseFont,\n            .foregroundColor: configuration.baseColor\n        ]\n    }\n    \n    mutating func visitDocument(_ document: Document) {\n        for child in document.children {\n            visit(child)\n        }\n    }\n    \n    /// 段落\n    mutating func visitParagraph(_ paragraph: Paragraph) {\n        // 如果是在列表项中，段落不需要额外的换行\n        // 结尾已经有两个换行也不加了\n        if attributedString.length > 0, !(paragraph.parent is ListItem), !attributedString.string.hasSuffix(\"\\n\\n\") {\n            attributedString.append(NSAttributedString(string: \"\\n\\n\", attributes: attributes))\n        }\n        \n        for child in paragraph.children {\n            visit(child)\n        }\n    }\n    \n    /// 文本\n    mutating func visitText(_ text: Text) {\n        attributedString.append(NSAttributedString(string: text.string, attributes: attributes))\n    }\n    \n    /// 加粗\n    mutating func visitStrong(_ strong: Strong) {\n        let originalAttributes = attributes\n        defer { attributes = originalAttributes }\n        \n        let originalFont = attributes[.font] as? UIFont ?? configuration.baseFont\n        attributes[.font] = originalFont.bold()\n        \n        for child in strong.children {\n            visit(child)\n        }\n    }\n    \n    /// 斜体\n    mutating func visitEmphasis(_ emphasis: Emphasis) {\n        let originalAttributes = attributes\n        defer { attributes = originalAttributes }\n        \n        let originalFont = attributes[.font] as? UIFont ?? configuration.baseFont\n        attributes[.font] = originalFont.italic()\n        \n        for child in emphasis.children {\n            visit(child)\n        }\n    }\n\n    /// 删除线\n    mutating func visitStrikethrough(_ strikethrough: Strikethrough) {\n        let originalAttributes = attributes\n        defer { attributes = originalAttributes }\n        \n        attributes[.strikethroughStyle] = NSUnderlineStyle.single.rawValue\n        \n        for child in strikethrough.children {\n            visit(child)\n        }\n    }\n    \n    /// 链接\n    mutating func visitLink(_ link: Link) {\n        let originalAttributes = attributes\n        defer { attributes = originalAttributes }\n        \n        attributes[.foregroundColor] = configuration.linkColor\n        attributes[.underlineStyle] = NSUnderlineStyle.single.rawValue\n        if let destination = link.destination {\n            attributes[.link] = destination\n        }\n        \n        for child in link.children {\n            visit(child)\n        }\n    }\n    \n    /// 行内代码\n    mutating func visitInlineCode(_ inlineCode: InlineCode) {\n        var currentAttributes = attributes\n        \n        currentAttributes[.font] = UIFont.monospacedSystemFont(ofSize: configuration.baseFont.pointSize, weight: .regular)\n        currentAttributes[.foregroundColor] = configuration.codeTextColor\n        currentAttributes[.backgroundColor] = configuration.codeBackgroundColor\n        \n        attributedString.append(NSAttributedString(string: inlineCode.code, attributes: currentAttributes))\n    }\n    \n    /// 代码块\n    mutating func visitCodeBlock(_ codeBlock: CodeBlock) {\n        if attributedString.length > 0, !attributedString.string.hasSuffix(\"\\n\\n\") {\n            attributedString.append(NSAttributedString(string: \"\\n\\n\", attributes: attributes))\n        }\n        \n        var currentAttributes = attributes\n        \n        currentAttributes[.font] = UIFont.monospacedSystemFont(ofSize: configuration.baseFont.pointSize, weight: .regular)\n        currentAttributes[.foregroundColor] = configuration.codeBlockTextColor\n        currentAttributes[.backgroundColor] = configuration.codeBackgroundColor\n        \n        attributedString.append(NSAttributedString(string: codeBlock.code, attributes: currentAttributes))\n        \n        attributedString.append(NSAttributedString(string: \"\\n\", attributes: [.font: configuration.baseFont]))\n    }\n\n    /// 标题\n    mutating func visitHeading(_ heading: Heading) {\n        if attributedString.length > 0, !attributedString.string.hasSuffix(\"\\n\\n\") {\n            attributedString.append(NSAttributedString(string: \"\\n\\n\", attributes: attributes))\n        }\n        \n        let originalAttributes = attributes\n        defer { attributes = originalAttributes }\n        \n        let level = heading.level\n        var size = configuration.baseFont.pointSize\n        var weight = UIFont.Weight.bold\n        \n        switch level {\n        case 1: size += 6; weight = .heavy\n        case 2: size += 4; weight = .bold\n        case 3: size += 2; weight = .semibold\n        default: break\n        }\n        \n        attributes[.font] = UIFont.systemFont(ofSize: size, weight: weight)\n        \n        for child in heading.children {\n            visit(child)\n        }\n    }\n    \n    /// 引用\n    mutating func visitBlockQuote(_ blockQuote: BlockQuote) {\n        if attributedString.length > 0 {\n            attributedString.append(NSAttributedString(string: \"\\n\", attributes: attributes))\n        }\n        \n        let originalAttributes = attributes\n        defer { attributes = originalAttributes }\n        \n        attributes[.foregroundColor] = configuration.quoteColor\n        \n        let paragraphStyle = NSMutableParagraphStyle()\n        paragraphStyle.firstLineHeadIndent = 10\n        paragraphStyle.headIndent = 10\n        attributes[.paragraphStyle] = paragraphStyle\n        \n        for child in blockQuote.children {\n            visit(child)\n        }\n    }\n    \n    /// 有序列表\n    mutating func visitOrderedList(_ orderedList: OrderedList) {\n        if attributedString.length > 0 && listStack.isEmpty {\n            attributedString.append(NSAttributedString(string: \"\\n\", attributes: attributes))\n        }\n        \n        listStack.append((type: .ordered, index: Int(orderedList.startIndex)))\n        \n        for child in orderedList.children {\n            visit(child)\n        }\n        \n        listStack.removeLast()\n    }\n    \n    /// 无序列表\n    mutating func visitUnorderedList(_ unorderedList: UnorderedList) {\n        if attributedString.length > 0 && listStack.isEmpty {\n            attributedString.append(NSAttributedString(string: \"\\n\", attributes: attributes))\n        }\n        \n        listStack.append((type: .unordered, index: 0))\n        \n        for child in unorderedList.children {\n            visit(child)\n        }\n        \n        listStack.removeLast()\n    }\n    \n    /// 列表项\n    mutating func visitListItem(_ listItem: ListItem) {\n        let originalAttributes = attributes\n        defer { attributes = originalAttributes }\n        \n        let level = CGFloat(listStack.count - 1)\n        let indent: CGFloat = 20\n        let baseIndent = level * indent\n        \n        let paragraphStyle = NSMutableParagraphStyle()\n        paragraphStyle.firstLineHeadIndent = baseIndent\n        paragraphStyle.headIndent = baseIndent + indent\n        paragraphStyle.paragraphSpacingBefore = 4\n        attributes[.paragraphStyle] = paragraphStyle\n        \n        var prefix = \"\"\n        if let last = listStack.last {\n            if last.type == .ordered {\n                prefix = \"\\(last.index). \"\n            } else if listItem.checkbox == nil {\n                prefix = \"• \"\n            }\n        }\n        if attributedString.length > 0 {\n            attributedString.append(NSAttributedString(string: \"\\n\", attributes: attributes))\n        }\n        attributedString.append(NSAttributedString(string: \"\\(prefix)\", attributes: attributes))\n        \n        if let checkbox = listItem.checkbox {\n            let imageName = checkbox == .checked ? \"checkmark.square\" : \"square\"\n            let font = attributes[.font] as? UIFont ?? configuration.baseFont\n            let color = attributes[.foregroundColor] as? UIColor ?? configuration.baseColor\n            \n            let symbolConfiguration = UIImage.SymbolConfiguration(font: font)\n            if let image = UIImage(systemName: imageName, withConfiguration: symbolConfiguration)?.withTintColor(color, renderingMode: .alwaysOriginal) {\n                let attachment = NSTextAttachment()\n                attachment.image = image\n                let y = (font.capHeight - image.size.height).rounded() / 2\n                attachment.bounds = CGRect(x: 0, y: y, width: image.size.width, height: image.size.height)\n                \n                attributedString.append(NSAttributedString(attachment: attachment))\n                attributedString.append(NSAttributedString(string: \" \", attributes: attributes))\n            } else {\n                let text = checkbox == .checked ? \"☑ \" : \"☐ \"\n                attributedString.append(NSAttributedString(string: text, attributes: attributes))\n            }\n        }\n        \n        for child in listItem.children {\n            visit(child)\n        }\n        \n        // 更新 listStack 索引\n        if !listStack.isEmpty {\n            var last = listStack[listStack.count - 1]\n            if last.type == .ordered {\n                last.index += 1\n                listStack[listStack.count - 1] = last\n            }\n        }\n    }\n\n    /// 图片\n    mutating func visitImage(_ image: Image) {\n        let originalAttributes = attributes\n        defer { attributes = originalAttributes }\n        \n        attributes[.foregroundColor] = configuration.linkColor\n        attributes[.underlineStyle] = NSUnderlineStyle.single.rawValue\n        \n        if let source = image.source {\n            attributes[.link] = source\n        }\n        \n        let altText = image.plainText.isEmpty ? \"image\".localized : image.plainText\n        attributedString.append(NSAttributedString(string: \"[\\(altText)]\", attributes: attributes))\n    }\n\n    /// 软换行渲染为空格\n    mutating func visitSoftBreak(_ softBreak: SoftBreak) {\n        attributedString.append(NSAttributedString(string: \" \", attributes: attributes))\n    }\n\n    /// 硬换行\n    mutating func visitLineBreak(_ lineBreak: LineBreak) {\n        attributedString.append(NSAttributedString(string: \"\\n\", attributes: attributes))\n    }\n}\n"
  },
  {
    "path": "Common/Moya/BarkApi.swift",
    "content": "//\n//  BarkApi.swift\n//  Bark\n//\n//  Created by huangfeng on 2018/6/25.\n//  Copyright © 2018 Fin. All rights reserved.\n//\n\nimport UIKit\n\nenum BarkApi {\n    case ping(baseURL: String?)\n    case register(address: String, key: String?, devicetoken: String) // 注册设备\n}\n\nextension BarkApi: BarkTargetType {\n    var baseURL: URL {\n        switch self {\n        case let .ping(urlStr):\n            if let url = URL(string: urlStr ?? \"\") {\n                return url\n            }\n        case let .register(address, _, _):\n            if let url = try? address.asURL() {\n                return url\n            }\n        }\n        return try! ServerManager.shared.currentServer.address.asURL()\n    }\n\n    var parameters: [String: Any]? {\n        switch self {\n        case let .register(_, key, devicetoken):\n            var params = [\"devicetoken\": devicetoken]\n            if let key = key {\n                params[\"key\"] = key\n            }\n            return params\n        default:\n            return nil\n        }\n    }\n\n    var path: String {\n        switch self {\n        case .ping:\n            return \"/ping\"\n        case .register:\n            return \"/register\"\n        }\n    }\n}\n"
  },
  {
    "path": "Common/Moya/BarkTargetType.swift",
    "content": "//\n//  BarkTargetType.swift\n//  Bark\n//\n//  Created by huangfeng on 2018/6/25.\n//  Copyright © 2018 Fin. All rights reserved.\n//\n\nimport Moya\nimport RxSwift\nimport UIKit\n\n// 保存全局Providers\nprivate var retainProviders: [String: Any] = [:]\n\nprotocol BarkTargetType: TargetType {\n    var parameters: [String: Any]? { get }\n}\n\nextension BarkTargetType {\n    var headers: [String: String]? {\n        return nil\n    }\n\n    var baseURL: URL {\n        return URL(string: ServerManager.shared.currentServer.address)!\n    }\n    \n    var method: Moya.Method {\n        return .get\n    }\n    \n    var parameterEncoding: ParameterEncoding {\n        return URLEncoding.default\n    }\n    \n    var sampleData: Data {\n        return Data()\n    }\n    \n    var task: Task {\n        return requestTaskWithParameters\n    }\n    \n    var requestTaskWithParameters: Task {\n        // 默认参数\n        var defaultParameters: [String: Any] = [:]\n        // 协议参数\n        if let parameters = self.parameters {\n            for (key, value) in parameters {\n                defaultParameters[key] = value\n            }\n        }\n        return Task.requestParameters(parameters: defaultParameters, encoding: parameterEncoding)\n    }\n    \n    /// 实现此协议的类，将自动获得用该类实例化的 provider 对象\n    static var provider: RxSwift.Reactive<MoyaProvider<Self>> {\n        let key = \"\\(Self.self)\"\n        if let provider = retainProviders[key] as? RxSwift.Reactive<MoyaProvider<Self>> {\n            return provider\n        }\n        let provider = Self.weakProvider\n        retainProviders[key] = provider\n        return provider\n    }\n    \n    /// 不被全局持有的 Provider ，使用时，需要持有它，否则将立即释放，请求随即终止\n    static var weakProvider: RxSwift.Reactive<MoyaProvider<Self>> {\n        var plugins: [PluginType] = []\n        #if DEBUG\n        plugins.append(LogPlugin())\n        #endif\n        let provider = MoyaProvider<Self>(plugins: plugins)\n        return provider.rx\n    }\n}\n\npublic extension RxSwift.Reactive where Base: MoyaProviderType {\n    func request(_ token: Base.Target, callbackQueue: DispatchQueue? = nil) -> Observable<Response> {\n        return Single.create { [weak base] single in\n            let cancellableToken = base?.request(token, callbackQueue: callbackQueue, progress: nil) { result in\n                switch result {\n                case let .success(response):\n                    single(.success(response))\n                case let .failure(error):\n                    single(.failure(error))\n                }\n            }\n            \n            return Disposables.create {\n                cancellableToken?.cancel()\n            }\n        }.asObservable()\n    }\n}\n\nprivate class LogPlugin: PluginType {\n    func willSend(_ request: RequestType, target: TargetType) {\n        print(\"\\n-------------------\\n准备请求: \\(target.path)\")\n        print(\"请求方式: \\(target.method.rawValue)\")\n        if let params = (target as? BarkTargetType)?.parameters {\n            print(params)\n        }\n        print(\"\\n\")\n    }\n\n    func didReceive(_ result: Result<Response, MoyaError>, target: TargetType) {\n        print(\"\\n-------------------\\n请求结束: \\(target.path)\")\n        if let data = try? result.get().data, let resutl = String(data: data, encoding: String.Encoding.utf8) {\n            print(\"请求结果: \\(resutl)\")\n        }\n        print(\"\\n\")\n    }\n}\n"
  },
  {
    "path": "Common/Moya/Observable+Extension.swift",
    "content": "//\n//  Observable+Extension.swift\n//  Bark\n//\n//  Created by huangfeng on 2018/6/25.\n//  Copyright © 2018 Fin. All rights reserved.\n//\n\nimport UIKit\n\nimport Moya\nimport ObjectMapper\nimport RxSwift\nimport SwiftyJSON\nimport UIKit\n\nextension Observable where Element: Moya.Response {\n    /// 过滤 HTTP 错误，例如超时，请求失败等\n    func filterHttpError() -> Observable<Result<Element, ApiError>> {\n        return catchAndReturn(Element(statusCode: 599, data: Data()))\n            .map { response -> Result<Element, ApiError> in\n                if (200...209) ~= response.statusCode {\n                    return .success(response)\n                } else {\n                    return .failure(ApiError.Error(info: \"网络错误\"))\n                }\n            }\n    }\n    \n    /// 过滤逻辑错误，例如协议里返回 错误CODE\n    func filterResponseError() -> Observable<Result<JSON, ApiError>> {\n        return filterHttpError()\n            .map { response -> Result<JSON, ApiError> in\n                switch response {\n                case .success(let element):\n                    do {\n                        let json = try JSON(data: element.data)\n                        \n                        if let codeStr = json[\"code\"].rawString(),\n                           let code = Int(codeStr),\n                           code == 200\n                        {\n                            return .success(json)\n                        } else {\n                            var msg: String = \"\"\n                            if json[\"message\"].exists() {\n                                msg = json[\"message\"].rawString()!\n                            }\n                            return .failure(ApiError.Error(info: msg))\n                        }\n                    } catch {\n                        return .failure(ApiError.Error(info: error.rawString()))\n                    }\n                case .failure(let error):\n                    return .failure(ApiError.Error(info: error.rawString()))\n                }\n            }\n    }\n    \n    /// 将Response 转换成 JSON Model\n    ///\n    /// - Parameters:\n    ///   - typeName: 要转换的Model Class\n    ///   - dataPath: 从哪个节点开始转换，例如 [\"data\",\"links\"]\n    func mapResponseToObj<T: Mappable>(_ typeName: T.Type, dataPath: [String] = [\"data\"]) -> Observable<Result<T, ApiError>> {\n        return filterResponseError().map { json in\n            switch json {\n            case .success(let json):\n                var rootJson = json\n                if dataPath.count > 0 {\n                    rootJson = rootJson[dataPath]\n                }\n                if let model: T = self.resultFromJSON(json: rootJson) {\n                    return .success(model)\n                } else {\n                    return .failure(ApiError.Error(info: \"json 转换失败\"))\n                }\n            case .failure(let error):\n                return .failure(error)\n            }\n        }\n    }\n    \n    /// 将Response 转换成 JSON Model Array\n    func mapResponseToObjArray<T: Mappable>(_ type: T.Type, dataPath: [String] = [\"data\"]) -> Observable<Result<[T], ApiError>> {\n        return filterResponseError().map { json in\n            switch json {\n            case .success(let json):\n                var rootJson = json\n                if dataPath.count > 0 {\n                    rootJson = rootJson[dataPath]\n                }\n                var result = [T]()\n                guard let jsonArray = rootJson.array else {\n                    return .failure(ApiError.Error(info: \"Root Json 不是 Array\"))\n                }\n                \n                for json in jsonArray {\n                    if let jsonModel: T = self.resultFromJSON(json: json) {\n                        result.append(jsonModel)\n                    } else {\n                        return .failure(ApiError.Error(info: \"json 转换失败\"))\n                    }\n                }\n                \n                return .success(result)\n            case .failure(let error):\n                return .failure(error)\n            }\n        }\n    }\n    \n    private func resultFromJSON<T: Mappable>(jsonString: String) -> T? {\n        return T(JSONString: jsonString)\n    }\n\n    private func resultFromJSON<T: Mappable>(json: JSON) -> T? {\n        if let str = json.rawString() {\n            return resultFromJSON(jsonString: str)\n        }\n        return nil\n    }\n}\n"
  },
  {
    "path": "Common/Operators.swift",
    "content": "//\n//  Operators.swift\n//  RxExample\n//\n//  Created by Krunoslav Zaher on 12/6/15.\n//  Copyright © 2015 Krunoslav Zaher. All rights reserved.\n//\nimport RxCocoa\nimport RxSwift\n#if os(iOS)\nimport UIKit\n#elseif os(macOS)\nimport AppKit\n#endif\n\n// Two way binding operator between control property and relay, that's all it takes.\ninfix operator <->: DefaultPrecedence\n\n#if os(iOS)\nfunc nonMarkedText(_ textInput: UITextInput) -> String? {\n    let start = textInput.beginningOfDocument\n    let end = textInput.endOfDocument\n\n    guard let rangeAll = textInput.textRange(from: start, to: end),\n          let text = textInput.text(in: rangeAll)\n    else {\n        return nil\n    }\n\n    guard let markedTextRange = textInput.markedTextRange else {\n        return text\n    }\n\n    guard let startRange = textInput.textRange(from: start, to: markedTextRange.start),\n          let endRange = textInput.textRange(from: markedTextRange.end, to: end)\n    else {\n        return text\n    }\n\n    return (textInput.text(in: startRange) ?? \"\") + (textInput.text(in: endRange) ?? \"\")\n}\n\nfunc <-> <Base>(textInput: TextInput<Base>, relay: BehaviorRelay<String>) -> Disposable {\n    let bindToUIDisposable = relay.bind(to: textInput.text)\n\n    let bindToRelay = textInput.text\n        .subscribe(onNext: { [weak base = textInput.base] _ in\n            guard let base = base else {\n                return\n            }\n\n            let nonMarkedTextValue = nonMarkedText(base)\n\n            /**\n             In some cases `textInput.textRangeFromPosition(start, toPosition: end)` will return nil even though the underlying\n             value is not nil. This appears to be an Apple bug. If it's not, and we are doing something wrong, please let us know.\n             The can be reproed easily if replace bottom code with\n\n             if nonMarkedTextValue != relay.value {\n                relay.accept(nonMarkedTextValue ?? \"\")\n             }\n             and you hit \"Done\" button on keyboard.\n             */\n            if let nonMarkedTextValue = nonMarkedTextValue, nonMarkedTextValue != relay.value {\n                relay.accept(nonMarkedTextValue)\n            }\n        }, onCompleted: {\n            bindToUIDisposable.dispose()\n        })\n\n    return Disposables.create(bindToUIDisposable, bindToRelay)\n}\n#endif\n\nfunc <-> <T>(property: ControlProperty<T>, relay: BehaviorRelay<T>) -> Disposable {\n    if T.self == String.self {\n#if DEBUG && !os(macOS)\n        fatalError(\"It is ok to delete this message, but this is here to warn that you are maybe trying to bind to some `rx.text` property directly to relay.\\n\" +\n            \"That will usually work ok, but for some languages that use IME, that simplistic method could cause unexpected issues because it will return intermediate results while text is being inputed.\\n\" +\n            \"REMEDY: Just use `textField <-> relay` instead of `textField.rx.text <-> relay`.\\n\" +\n            \"Find out more here: https://github.com/ReactiveX/RxSwift/issues/649\\n\"\n        )\n#endif\n    }\n\n    let bindToUIDisposable = relay.bind(to: property)\n    let bindToRelay = property\n        .subscribe(onNext: { n in\n            relay.accept(n)\n        }, onCompleted: {\n            bindToUIDisposable.dispose()\n        })\n\n    return Disposables.create(bindToUIDisposable, bindToRelay)\n}\n"
  },
  {
    "path": "Common/RealmConfiguration.swift",
    "content": "//\n//  RealmConfiguration.swift\n//  NotificationServiceExtension\n//\n//  Created by huangfeng on 2024/5/29.\n//  Copyright © 2024 Fin. All rights reserved.\n//\n\n@_exported import RealmSwift\nimport UIKit\n\nlet kRealmDefaultConfiguration = {\n    let fileUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first?.appendingPathComponent(\"bark.realm\")\n    let config = Realm.Configuration(\n        fileURL: fileUrl,\n        schemaVersion: 17,\n        migrationBlock: { migration, oldSchemaVersion in\n            switch oldSchemaVersion {\n            case 0...13:\n                migration.enumerateObjects(ofType: Message.className()) { oldObject, newObject in\n                    guard let obj = oldObject else {\n                        return\n                    }\n                    guard let isDeleted = obj[\"isDeleted\"] as? Bool else {\n                        return\n                    }\n                    // 旧版软删除的数据，迁移到新版时硬删除掉，新版不再过滤 isDeleted 字段\n                    if isDeleted, let newObject {\n                        migration.delete(newObject)\n                    }\n                }\n            default:\n                break\n            }\n        }\n    )\n    return config\n}()\n"
  },
  {
    "path": "Common/Reusable.swift",
    "content": "//\n//  Reusable.swift\n//  Bark\n//\n//  Created by huangfeng on 2020/11/17.\n//  Copyright © 2020 Fin. All rights reserved.\n//\n\nimport RxCocoa\nimport RxSwift\nimport UIKit\n\nprivate var prepareForReuseBag: Int8 = 0\n\n@objc public protocol Reusable: AnyObject {\n    func prepareForReuse()\n}\n\nextension UITableViewCell: Reusable {}\nextension UITableViewHeaderFooterView: Reusable {}\nextension UICollectionReusableView: Reusable {}\n\nextension Reactive where Base: Reusable {\n    var reuseBag: DisposeBag {\n        MainScheduler.ensureExecutingOnScheduler()\n\n        if let bag = objc_getAssociatedObject(base, &prepareForReuseBag) as? DisposeBag {\n            return bag\n        }\n\n        let bag = DisposeBag()\n        objc_setAssociatedObject(base, &prepareForReuseBag, bag, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)\n\n        _ = sentMessage(#selector(Base.prepareForReuse))\n            .take(until: deallocated)\n            .subscribe(onNext: { [weak base] _ in\n                guard let strongBase = base else {\n                    return\n                }\n                let newBag = DisposeBag()\n                objc_setAssociatedObject(strongBase, &prepareForReuseBag, newBag, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)\n            })\n\n        return bag\n    }\n}\n"
  },
  {
    "path": "Common/ServerManager.swift",
    "content": "//\n//  ServerManager.swift\n//  Bark\n//\n//  Created by huangfeng on 2018/3/21.\n//  Copyright © 2018年 Fin. All rights reserved.\n//\n\nimport RxSwift\nimport SwiftUI\nimport UIKit\n\nlet defaultServer = \"https://api.day.app\"\n\nclass Server: Codable {\n    let id: String\n    let address: String\n    var key: String\n    var state: Client.ClienState\n    var name: String?\n    \n    var host: String {\n        return URL(string: address)?.host ?? \"\"\n    }\n    \n    init(id: String = UUID().uuidString, address: String, key: String, state: Client.ClienState = .ok) {\n        self.id = id\n        self.address = address\n        self.key = key\n        self.state = state\n    }\n    \n    enum CodingKeys: String, CodingKey {\n        case id\n        case address\n        case key\n        case name\n    }\n    \n    // 解码\n    required init(from decoder: Decoder) throws {\n        let container = try decoder.container(keyedBy: CodingKeys.self)\n        id = try container.decode(String.self, forKey: .id)\n        address = try container.decode(String.self, forKey: .address)\n        key = try container.decode(String.self, forKey: .key)\n        name = try? container.decode(String?.self, forKey: .name)\n        state = .ok\n    }\n}\n\nclass ServerManager: NSObject {\n    static let shared = ServerManager()\n    override private init() {\n        if let servers: [Server] = Settings[.servers] {\n            self.servers = servers\n        }\n\n        if servers.count <= 0 {\n            servers = [Server(id: UUID().uuidString, address: defaultServer, key: \"\")]\n        }\n        self.currentServer = servers[0]\n\n        super.init()\n\n        // 将老版本数据转换成新版本\n        if let key = Settings[.key] {\n            let address = Settings[.currentServer] ?? defaultServer\n            let server = Server(id: UUID().uuidString, address: address, key: key)\n\n            self.servers = []\n            self.addServer(server: server)\n\n            Settings[.currentServerId] = server.id\n\n            // 清空老版本数据\n            Settings[.currentServer] = nil\n            Settings[.key] = nil\n        }\n\n        if let currentServerId = Settings[.currentServerId] {\n            self.setCurrentServer(serverId: currentServerId)\n        }\n    }\n\n    /// 所有的 server\n    var servers: [Server] = []\n    /// 当前选中的 server ，在教程页显示。\n    private(set) var currentServer: Server\n\n    /// 更改当前选中的 server\n    func setCurrentServer(serverId: String) {\n        if let server = servers.first(where: { $0.id == serverId }) {\n            currentServer = server\n        } else {\n            currentServer = servers.first!\n        }\n        Settings[.currentServerId] = serverId\n    }\n\n    /// 添加新的 server\n    func addServer(server: Server) {\n        self.servers.append(server)\n        saveServers()\n    }\n\n    func updateServerKey(server: Server) {\n        let foundServer = self.servers.first { $0.id == server.id }\n        foundServer?.key = server.key\n        saveServers()\n    }\n    \n    /// 移除 server，移除后如果 server 为`空`, `会新增一个默认server`\n    func removeServer(server: Server) {\n        self.servers.removeAll { $0.id == server.id }\n        if self.servers.count <= 0 {\n            self.servers.append(\n                Server(id: UUID().uuidString, address: defaultServer, key: \"\")\n            )\n        }\n        if self.currentServer.id == server.id {\n            self.setCurrentServer(serverId: self.servers[0].id)\n        }\n        saveServers()\n    }\n\n    /// 保存 servers\n    func saveServers() {\n        Settings[.servers] = self.servers\n    }\n\n    @available(*, unavailable)\n    required init?(coder aDecoder: NSCoder) {\n        fatalError(\"init(coder:) has not been implemented\")\n    }\n\n    var dispose: Disposable?\n    /// 同步所有 server\n    func syncAllServers() {\n        guard let token = Client.shared.deviceToken.value, token.count > 0 else {\n            return\n        }\n        dispose?.dispose()\n\n        let apis = servers.map { server in\n            BarkApi.provider.request(\n                .register(\n                    address: server.address,\n                    key: server.key,\n                    devicetoken: token\n                ))\n                .filterResponseError()\n                .map { result -> (Server, String, Client.ClienState) in\n\n                    switch result {\n                    case .success(let json):\n                        if let key = json[\"data\", \"key\"].rawString() {\n                            return (server, key, .ok)\n                        } else {\n                            return (server, \"\", .serverError(error: .Error(info: \"key not found\")))\n                        }\n                    case .failure(let error):\n                        return (server, \"\", .serverError(error: error))\n                    }\n                }.catch { error in\n                    Observable.just((server, \"\", .serverError(error: .Error(info: error.localizedDescription))))\n                }\n        }\n\n        dispose = Observable\n            .merge(apis)\n            .subscribe { result in\n                // 更新所有的 server 状态\n                if result.2 == .ok {\n                    result.0.key = result.1\n                }\n                result.0.state = result.2\n\n                // 通知客户端 当前 server 状态改变\n                if result.0.id == self.currentServer.id {\n                    Client.shared.state.accept(result.2)\n                }\n            } onError: { _ in\n\n            } onCompleted: {\n                self.saveServers()\n            }\n    }\n    \n    func setServerName(server: Server, name: String?) {\n        server.name = name\n        saveServers()\n    }\n}\n"
  },
  {
    "path": "Common/SharedDefines.swift",
    "content": "//\n//  SharedDefines.swift\n//  Bark\n//\n//  Created by huangfeng on 2024/7/26.\n//  Copyright © 2024 Fin. All rights reserved.\n//\n\nimport Foundation\n\nlet kBarkSoundPrefix = \"bark.sounds.30s\"\n"
  },
  {
    "path": "Common/String+Extension.swift",
    "content": "//\n//  String+Extension.swift\n//  Bark\n//\n//  Created by huangfeng on 2018/6/26.\n//  Copyright © 2018 Fin. All rights reserved.\n//\n\nimport UIKit\n\nextension String {\n    // 将原始的url编码为合法的url\n    func urlEncoded() -> String {\n        let encodeUrlString = self.addingPercentEncoding(withAllowedCharacters:\n            .urlQueryAllowed)\n        return encodeUrlString ?? \"\"\n    }\n\n    // 将编码后的url转换回原始的url\n    func urlDecoded() -> String {\n        return self.removingPercentEncoding ?? \"\"\n    }\n}\n\n// MARK: - NSAttributedString\n\nextension String {\n    var bold: NSAttributedString {\n        return NSMutableAttributedString(string: self, attributes: [.font: UIFont.boldSystemFont(ofSize: UIFont.systemFontSize)])\n    }\n\n    var underline: NSAttributedString {\n        return NSAttributedString(string: self, attributes: [.underlineStyle: NSUnderlineStyle.single.rawValue])\n    }\n\n    var strikethrough: NSAttributedString {\n        return NSAttributedString(string: self, attributes: [.strikethroughStyle: NSNumber(value: NSUnderlineStyle.single.rawValue as Int)])\n    }\n\n    var italic: NSAttributedString {\n        return NSMutableAttributedString(string: self, attributes: [.font: UIFont.italicSystemFont(ofSize: UIFont.systemFontSize)])\n    }\n\n    func colored(with color: UIColor) -> NSAttributedString {\n        return NSMutableAttributedString(string: self, attributes: [.foregroundColor: color])\n    }\n}\n\n// MARK: - Format\n\nextension String {\n    func format(_ arguments: any CVarArg...) -> String {\n        return String(format: self, arguments)\n    }\n}\n\nextension String {\n    var localized: String {\n        return NSLocalizedString(self, comment: \"\")\n    }\n    \n    func localized(with arguments: CVarArg...) -> String {\n        return String(format: NSLocalizedString(self, comment: \"\"), arguments: arguments)\n    }\n}\n"
  },
  {
    "path": "Common/UIColor+Extension.swift",
    "content": "//\n//  UIColor+Extension.swift\n//  Bark\n//\n//  Created by huangfeng on 2018/6/25.\n//  Copyright © 2018 Fin. All rights reserved.\n//\n\nimport UIKit\n\nextension UIColor {\n    public convenience init(r255: CGFloat, g255: CGFloat, b255: CGFloat, a255: CGFloat = 255) {\n        self.init(red: r255/255, green: g255/255, blue: b255/255, alpha: a255/255)\n    }\n\n    class func image(color: UIColor, size: CGSize = CGSize(width: 1, height: 1)) -> UIImage {\n        UIGraphicsBeginImageContext(size)\n        let context = UIGraphicsGetCurrentContext()\n        context?.setFillColor(color.cgColor)\n        context?.fill(CGRect(origin: CGPoint.zero, size: size))\n\n        let image = UIGraphicsGetImageFromCurrentImageContext()\n        UIGraphicsEndImageContext()\n        return image! // context应该不会没get到吧~ 所以直接强解了\n    }\n\n    var image: UIImage {\n        return UIColor.image(color: self)\n    }\n}\n"
  },
  {
    "path": "Common/UIFont+Extension.swift",
    "content": "//\n//  UIFont+Extension.swift\n//  Bark\n//\n//  Created by huangfeng on 10/25/24.\n//  Copyright © 2024 Fin. All rights reserved.\n//\n\nimport UIKit\n\nextension UIFont {\n    class func preferredFont(ofSize size: CGFloat, weight: Weight = .regular) -> UIFont {\n        return UIFontMetrics.default.scaledFont(for: UIFont.systemFont(ofSize: size, weight: weight))\n    }\n}\n\nextension UIFont {\n    func bold() -> UIFont {\n        if let descriptor = self.fontDescriptor.withSymbolicTraits(.traitBold) {\n            return UIFont(descriptor: descriptor, size: self.pointSize)\n        }\n        return self\n    }\n    \n    func italic() -> UIFont {\n        if let descriptor = self.fontDescriptor.withSymbolicTraits(.traitItalic) {\n            return UIFont(descriptor: descriptor, size: self.pointSize)\n        }\n        return self\n    }\n}\n"
  },
  {
    "path": "Common/ViewModelType.swift",
    "content": "//\n//  ViewModelType.swift\n//  Bark\n//\n//  Created by huangfeng on 2020/11/17.\n//  Copyright © 2020 Fin. All rights reserved.\n//\n\nimport Foundation\nimport RxSwift\n\nprotocol ViewModelType {\n    associatedtype Input\n    associatedtype Output\n\n    func transform(input: Input) -> Output\n}\n\nclass ViewModel: NSObject {}\n"
  },
  {
    "path": "Controller/BarkNavigationController.swift",
    "content": "//\n//  BarkNavigationController.swift\n//  Bark\n//\n//  Created by huangfeng on 2018/6/25.\n//  Copyright © 2018 Fin. All rights reserved.\n//\n\nimport Material\nimport RxCocoa\nimport RxSwift\nimport UIKit\n\nclass BarkNavigationController: UINavigationController {\n    override func viewDidLoad() {\n        super.viewDidLoad()\n        self.navigationBar.prefersLargeTitles = true\n    }\n}\n\nclass BarkSnackbarController: SnackbarController {\n    override var childForStatusBarStyle: UIViewController? {\n        return self.rootViewController\n    }\n}\n\nenum TabPage: Int {\n    case unknown = -1\n    case service = 0\n    case messageHistory = 1\n    case settings = 2\n}\n\nclass StateRestoringTabBarContr: UITabBarController, UITabBarControllerDelegate {\n    // 标记当前显示的页面，再次点击相同的页面时当做页面点击事件。\n    var currentSelectedIndex: Int = -1 {\n        didSet {\n            guard currentSelectedIndex >= 0 else {\n                return\n            }\n            guard currentSelectedIndex < self.viewControllers?.count ?? 0 else {\n                return\n            }\n            guard currentSelectedIndex != oldValue else {\n                return\n            }\n            \n            if currentSelectedIndex != self.selectedIndex {\n                self.selectedIndex = currentSelectedIndex\n            }\n            \n            guard oldValue >= 0 else {\n                // 如果是 -1 代表是初始化时的赋值，不需要重复保存\n                return\n            }\n            \n            Settings[.selectedViewControllerIndex] = currentSelectedIndex\n        }\n    }\n\n    // 点击当前页面的 tabBarItem ， 可以用以点击刷新当前页面等操作\n    lazy var tabBarItemDidClick: PublishRelay<TabPage> = PublishRelay()\n\n    var isFirstAppear = true\n    override func viewWillAppear(_ animated: Bool) {\n        super.viewWillAppear(animated)\n        if isFirstAppear {\n            isFirstAppear = false\n\n            // 开启APP时，默认选择上次打开的页面\n            self.currentSelectedIndex = Settings[.selectedViewControllerIndex] ?? 0\n\n            self.rx.didSelect.subscribe(onNext: { [weak self] vc in\n                guard let self = self else { return }\n                if self.currentSelectedIndex == self.selectedIndex {\n                    self.tabBarItemDidClick.accept(TabPage(rawValue: self.selectedIndex) ?? .unknown)\n                }\n                self.currentSelectedIndex = self.selectedIndex\n            }).disposed(by: rx.disposeBag)\n        }\n    }\n}\n"
  },
  {
    "path": "Controller/BarkSFSafariViewController.swift",
    "content": "//\n//  BarkSFSafariViewController.swift\n//  Bark\n//\n//  Created by huangfeng on 2018/6/26.\n//  Copyright © 2018 Fin. All rights reserved.\n//\n\nimport SafariServices\nimport UIKit\n\nclass BarkSFSafariViewController: SFSafariViewController {\n    override var preferredStatusBarStyle: UIStatusBarStyle {\n        return .default\n    }\n\n    deinit {\n        if #available(iOS 16.0, *) {\n            Task {\n                await SFSafariViewController.DataStore.default.clearWebsiteData()\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Controller/BarkSplitViewController.swift",
    "content": "//\n//  BarkSplitViewController.swift\n//  Bark\n//\n//  Created by sidguan on 2024/6/30.\n//  Copyright © 2024 Fin. All rights reserved.\n//\n\nimport Material\nimport UIKit\n\n@available(iOS 14, *)\nclass BarkSplitViewController: UISplitViewController {\n    let sectionViewController = SectionViewController_iPad(viewModel: SectionViewModel())\n    // Compact 下替换显示成 BarkTabBarController\n    let compactController = BarkTabBarController()\n    override func viewDidLoad() {\n        super.viewDidLoad()\n        self.preferredDisplayMode = .oneBesideSecondary\n        self.preferredSplitBehavior = .tile\n        self.delegate = self\n        initViewControllers()\n    }\n\n    func initViewControllers() {\n        self.setViewController(sectionViewController, for: .primary)\n        // 设置默认打开页面\n        let index: Int = Settings[.selectedViewControllerIndex] ?? 0\n        self.setViewController(sectionViewController.viewControllers[index], for: .secondary)\n        DispatchQueue.main.async {\n            self.sectionViewController.tableView.selectRow(at: IndexPath(row: index, section: 0), animated: false, scrollPosition: .none)\n        }\n        self.setViewController(compactController, for: .compact)\n    }\n}\n\n@available(iOS 14, *)\nextension BarkSplitViewController: UISplitViewControllerDelegate {\n    // 同步 sectionViewController 和 compactController 当前显示页面\n    func splitViewControllerDidCollapse(_ svc: UISplitViewController) {\n        guard let index: Int = Settings[.selectedViewControllerIndex] else {\n            return\n        }\n        self.compactController.selectedIndex = index\n    }\n\n    func splitViewControllerDidExpand(_ svc: UISplitViewController) {\n        guard let index: Int = Settings[.selectedViewControllerIndex] else {\n            return\n        }\n        self.sectionViewController.tableView.selectRow(at: IndexPath(row: index, section: 0), animated: false, scrollPosition: .none)\n        self.setViewController(self.sectionViewController.viewControllers[index], for: .secondary)\n    }\n}\n"
  },
  {
    "path": "Controller/BarkTabBarController.swift",
    "content": "//\n//  BarkTabBarController.swift\n//  Bark\n//\n//  Created by huangfeng on 2024/8/20.\n//  Copyright © 2024 Fin. All rights reserved.\n//\n\nimport Material\nimport UIKit\n\nclass BarkTabBarController: StateRestoringTabBarContr {\n    override func viewDidLoad() {\n        super.viewDidLoad()\n        self.tabBar.tintColor = BKColor.grey.darken4\n        \n        self.viewControllers = [\n            BarkNavigationController(rootViewController: HomeViewController(viewModel: HomeViewModel())),\n            BarkNavigationController(rootViewController: MessageListViewController(viewModel: MessageListViewModel())),\n            BarkNavigationController(rootViewController: MessageSettingsViewController(viewModel: MessageSettingsViewModel()))\n        ]\n        \n        let tabBarItems = [UITabBarItem(title: \"service\".localized, image: UIImage(named: \"baseline_gite_black_24pt\"), tag: 0),\n                           UITabBarItem(title: \"historyMessage\".localized, image: Icon.history, tag: 1),\n                           UITabBarItem(title: \"settings\".localized, image: UIImage(named: \"baseline_manage_accounts_black_24pt\"), tag: 2)]\n        for (index, viewController) in self.viewControllers!.enumerated() {\n            viewController.tabBarItem = tabBarItems[index]\n        }\n    }\n}\n"
  },
  {
    "path": "Controller/BaseViewController.swift",
    "content": "//\n//  BaseViewController.swift\n//  Bark\n//\n//  Created by huangfeng on 2018/6/25.\n//  Copyright © 2018 Fin. All rights reserved.\n//\n\nimport Material\nimport UIKit\n\nclass BaseViewController<T>: UIViewController where T: ViewModel {\n    let viewModel: T\n    init(viewModel: T) {\n        self.viewModel = viewModel\n        super.init(nibName: nil, bundle: nil)\n    }\n    \n    @available(*, unavailable)\n    required init?(coder: NSCoder) {\n        fatalError(\"init(coder:) has not been implemented\")\n    }\n    \n    override var preferredStatusBarStyle: UIStatusBarStyle {\n        return .lightContent\n    }\n    \n    override func viewDidLoad() {\n        super.viewDidLoad()\n        self.view.backgroundColor = BKColor.background.primary\n        \n        if UIDevice.current.userInterfaceIdiom == .pad {\n            navigationItem.largeTitleDisplayMode = .never\n        } else {\n            navigationItem.largeTitleDisplayMode = .automatic\n        }\n        makeUI()\n    }\n\n    var isViewModelBinded = false\n    override func viewWillAppear(_ animated: Bool) {\n        super.viewWillAppear(animated)\n        if !isViewModelBinded {\n            isViewModelBinded = true\n            self.bindViewModel()\n        }\n    }\n    \n    func makeUI() {}\n\n    func bindViewModel() {}\n}\n"
  },
  {
    "path": "Controller/CryptoSettingController.swift",
    "content": "//\n//  CryptoSettingController.swift\n//  Bark\n//\n//  Created by huangfeng on 2022/11/10.\n//  Copyright © 2022 Fin. All rights reserved.\n//\n\nimport RxSwift\nimport UIKit\n\nclass CryptoSettingController: BaseViewController<CryptoSettingViewModel> {\n    let algorithmFeild = DropBoxView(values: [\"AES128\", \"AES192\", \"AES256\"])\n    let modeFeild = DropBoxView(values: [\"CBC\", \"ECB\", \"GCM\"])\n    let paddingField = DropBoxView(values: [\"pkcs7\"])\n\n    let keyTextField: BorderTextField = {\n        let textField = BorderTextField(title: \"Key\")\n        textField.font = UIFont.preferredFont(ofSize: 14)\n        textField.adjustsFontForContentSizeCategory = true\n        textField.placeholder = \"enterKey\".localized(with: 16)\n        return textField\n    }()\n\n    let ivTextField: BorderTextField = {\n        let textField = BorderTextField(title: \"IV\")\n        textField.font = UIFont.preferredFont(ofSize: 14)\n        textField.adjustsFontForContentSizeCategory = true\n        return textField\n    }()\n\n    let doneButton: BKButton = {\n        let btn = BKButton()\n        btn.setTitle(\"done\".localized, for: .normal)\n        btn.setTitleColor(BKColor.lightBlue.darken3, for: .normal)\n        btn.frame = CGRect(x: 0, y: 0, width: 40, height: 40)\n        btn.fontSize = 14\n        return btn\n    }()\n\n    let copyButton: UIButton = {\n        let btn = GradientButton()\n        btn.setTitle(\"copyExample\".localized, for: .normal)\n        btn.setTitleColor(UIColor.white, for: .normal)\n        btn.titleLabel?.font = UIFont.preferredFont(ofSize: 14, weight: .medium)\n        btn.titleLabel?.adjustsFontForContentSizeCategory = true\n        btn.layer.cornerRadius = 8\n        btn.clipsToBounds = true\n        btn.applyGradient(\n            withColours: [\n                UIColor(r255: 36, g255: 51, b255: 236),\n                UIColor(r255: 70, g255: 44, b255: 233)\n            ],\n            gradientOrientation: .horizontal\n        )\n        return btn\n    }()\n\n    let scrollView = UIScrollView()\n    override func viewDidAppear(_ animated: Bool) {\n        super.viewDidAppear(animated)\n    }\n\n    override func makeUI() {\n        self.title = \"encryptionSettings\".localized\n        self.navigationItem.setRightBarButtonItem(item: UIBarButtonItem(customView: doneButton))\n\n        self.view.addSubview(scrollView)\n        scrollView.snp.makeConstraints { make in\n            make.edges.equalToSuperview()\n        }\n\n        func getTitleLabel(title: String) -> UILabel {\n            let label = UILabel()\n            label.font = UIFont.preferredFont(ofSize: 14)\n            label.adjustsFontForContentSizeCategory = true\n            label.textColor = BKColor.grey.darken4\n            label.text = title\n            return label\n        }\n\n        let algorithmLabel = getTitleLabel(title: \"algorithm\".localized)\n        let modeLabel = getTitleLabel(title: \"mode\".localized)\n        let paddingLabel = getTitleLabel(title: \"Padding\")\n        let keyLabel = getTitleLabel(title: \"Key\")\n        let ivLabel = getTitleLabel(title: \"Iv\")\n\n        self.scrollView.addSubview(algorithmLabel)\n        self.scrollView.addSubview(algorithmFeild)\n\n        self.scrollView.addSubview(modeLabel)\n        self.scrollView.addSubview(modeFeild)\n\n        self.scrollView.addSubview(paddingLabel)\n        self.scrollView.addSubview(paddingField)\n\n        self.scrollView.addSubview(keyLabel)\n        self.scrollView.addSubview(keyTextField)\n\n        self.scrollView.addSubview(ivLabel)\n        self.scrollView.addSubview(ivTextField)\n\n        self.scrollView.addSubview(copyButton)\n\n        self.view.backgroundColor = UIColor.white\n\n        algorithmLabel.snp.makeConstraints { make in\n            make.top.equalTo(24)\n            make.left.equalTo(24)\n        }\n        algorithmFeild.snp.makeConstraints { make in\n            make.top.equalTo(algorithmLabel.snp.bottom).offset(5)\n            make.left.equalTo(20)\n            make.right.equalTo(-20)\n            make.height.equalTo(45)\n            make.width.equalToSuperview().offset(-40)\n        }\n\n        modeLabel.snp.makeConstraints { make in\n            make.top.equalTo(algorithmFeild.snp.bottom).offset(20)\n            make.left.equalTo(algorithmLabel)\n        }\n        modeFeild.snp.makeConstraints { make in\n            make.left.right.height.equalTo(algorithmFeild)\n            make.top.equalTo(modeLabel.snp.bottom).offset(5)\n        }\n\n        paddingLabel.snp.makeConstraints { make in\n            make.top.equalTo(modeFeild.snp.bottom).offset(20)\n            make.left.equalTo(algorithmLabel)\n        }\n        paddingField.snp.makeConstraints { make in\n            make.left.right.height.equalTo(modeFeild)\n            make.top.equalTo(paddingLabel.snp.bottom).offset(5)\n        }\n\n        keyLabel.snp.makeConstraints { make in\n            make.top.equalTo(paddingField.snp.bottom).offset(20)\n            make.left.equalTo(algorithmLabel)\n        }\n        keyTextField.snp.makeConstraints { make in\n            make.left.right.height.equalTo(paddingField)\n            make.top.equalTo(keyLabel.snp.bottom).offset(5)\n        }\n\n        ivLabel.snp.makeConstraints { make in\n            make.top.equalTo(keyTextField.snp.bottom).offset(20)\n            make.left.equalTo(algorithmLabel)\n        }\n        ivTextField.snp.makeConstraints { make in\n            make.left.right.height.equalTo(keyTextField)\n            make.top.equalTo(ivLabel.snp.bottom).offset(5)\n        }\n\n        copyButton.snp.makeConstraints { make in\n            make.left.equalTo(ivTextField)\n            make.right.equalTo(ivTextField)\n            make.height.equalTo(42)\n            make.top.equalTo(ivTextField.snp.bottom).offset(25)\n            make.bottom.equalToSuperview().offset(-20)\n        }\n\n        self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(resign)))\n    }\n\n    @objc func resign() {\n        self.view.endEditing(true)\n    }\n\n    override func viewDidLoad() {\n        super.viewDidLoad()\n        self.view.backgroundColor = BKColor.white\n    }\n\n    override func bindViewModel() {\n        func getFieldValues() -> CryptoSettingFields {\n            return CryptoSettingFields(\n                algorithm: self.algorithmFeild.currentValue!,\n                mode: self.modeFeild.currentValue!,\n                padding: self.paddingField.currentValue!,\n                key: self.keyTextField.text,\n                iv: self.ivTextField.text\n            )\n        }\n\n        let output = viewModel.transform(input: CryptoSettingViewModel.Input(\n            algorithmChanged: self.algorithmFeild\n                .rx\n                .currentValueChanged\n                .compactMap { $0 }\n                .asDriver(onErrorDriveWith: .empty()),\n            modeChanged: self.modeFeild\n                .rx\n                .currentValueChanged\n                .compactMap { $0 }\n                .asDriver(onErrorDriveWith: .empty()),\n            copyScript: copyButton\n                .rx\n                .tap\n                .map { getFieldValues() }\n                .asDriver(onErrorDriveWith: .empty()),\n\n            done: doneButton\n                .rx\n                .tap\n                .map { getFieldValues() }\n                .asDriver(onErrorDriveWith: .empty())\n        ))\n\n        output.initial.drive(onNext: { [weak self] val in\n            self?.algorithmFeild.values = val.algorithmList.map { $0.rawValue }\n            self?.modeFeild.values = val.modeList\n            self?.paddingField.values = val.paddingList\n            if let fields = val.initialFields {\n                self?.algorithmFeild.currentValue = fields.algorithm\n                self?.modeFeild.currentValue = fields.mode\n                self?.paddingField.currentValue = fields.padding\n                self?.keyTextField.text = fields.key\n                self?.ivTextField.text = fields.iv\n            }\n            self?.setIvLengthPlaceholder(mode: self?.modeFeild.currentValue)\n        }).disposed(by: rx.disposeBag)\n\n        output.modeListChanged\n            .drive(self.modeFeild.rx.values)\n            .disposed(by: rx.disposeBag)\n\n        output.paddingListChanged\n            .drive(self.paddingField.rx.values)\n            .disposed(by: rx.disposeBag)\n\n        output.keyLengthChanged.drive(onNext: { [weak self] keyLength in\n            self?.keyTextField.placeholder = \"enterKey\".localized(with: keyLength)\n        }).disposed(by: rx.disposeBag)\n        \n        self.modeFeild\n            .rx\n            .currentValueChanged\n            .subscribe(onNext: { [weak self] val in\n                self?.setIvLengthPlaceholder(mode: val)\n            }).disposed(by: rx.disposeBag)\n\n        output.showSnackbar.drive(onNext: { text in\n            HUDError(text)\n        }).disposed(by: rx.disposeBag)\n\n        output.done.drive(onNext: { [weak self] in\n            self?.navigationController?.dismiss(animated: true)\n        }).disposed(by: rx.disposeBag)\n\n        output.copy.drive(onNext: { text in\n            UIPasteboard.general.string = text\n            HUDSuccess(\"Copy\".localized)\n        }).disposed(by: rx.disposeBag)\n    }\n    \n    private func setIvLengthPlaceholder(mode: String?) {\n        guard let mode else {\n            return\n        }\n        if let length = [\"CBC\": 16, \"GCM\": 12][mode] {\n            self.ivTextField.placeholder = \"enterIv\".localized(with: length)\n        } else {\n            self.ivTextField.placeholder = \"\"\n        }\n    }\n}\n"
  },
  {
    "path": "Controller/CryptoSettingViewModel.swift",
    "content": "//\n//  CryptoSettingViewModel.swift\n//  Bark\n//\n//  Created by huangfeng on 2022/11/10.\n//  Copyright © 2022 Fin. All rights reserved.\n//\n\nimport CryptoSwift\nimport Foundation\nimport RxCocoa\nimport RxSwift\n\nclass CryptoSettingViewModel: ViewModel, ViewModelType {\n    struct Input {\n        let algorithmChanged: Driver<String>\n        let modeChanged: Driver<String>\n        let copyScript: Driver<CryptoSettingFields>\n        let done: Driver<CryptoSettingFields>\n    }\n\n    struct Output {\n        let initial: Driver<(algorithmList: [Algorithm], modeList: [String], paddingList: [String], initialFields: CryptoSettingFields?)>\n        let modeListChanged: Driver<[String]>\n        let paddingListChanged: Driver<[String]>\n        let keyLengthChanged: Driver<Int>\n        let showSnackbar: Driver<String>\n        let done: Driver<Void>\n        let copy: Driver<String>\n    }\n\n    struct Dependencies {\n        let settingFieldRelay: BehaviorRelay<CryptoSettingFields?>\n        let deviceKey: Driver<String>\n        let serverAddress: Driver<String>\n    }\n\n    private let dependencies: Dependencies\n\n    init(dependencies: Dependencies =\n        Dependencies(\n            settingFieldRelay: CryptoSettingRelay.shared.fields,\n            // Key 好像没有对应的事件流，先“just”，懒得写了\n            deviceKey: Driver.just(ServerManager.shared.currentServer.key),\n            serverAddress: Driver.just(ServerManager.shared.currentServer.address)\n        )\n    ) {\n        self.dependencies = dependencies\n    }\n\n    func transform(input: Input) -> Output {\n        let showSnackbar = PublishRelay<String>()\n\n        let modeList = input\n            .algorithmChanged\n            .compactMap { Algorithm(rawValue: $0) }\n            .map { $0.modes }\n\n        let paddingList = input\n            .modeChanged\n            .map { mode in\n                if mode == \"GCM\" {\n                    return [\"noPadding\"]\n                } else {\n                    return [\"pkcs7\"]\n                }\n            }\n\n        let keyLength =\n            Driver.merge([\n                Driver.just(dependencies.settingFieldRelay.value)\n                    .compactMap { $0 }\n                    .compactMap { Algorithm(rawValue: $0.algorithm)?.keyLength },\n                input\n                    .algorithmChanged\n                    .compactMap { Algorithm(rawValue: $0)?.keyLength }\n            ])\n\n        // 保存配置\n        let done = input.done\n            .filter { fields in\n                do {\n                    _ = try AESCryptoModel(cryptoFields: fields)\n                    return true\n                } catch {\n                    showSnackbar.accept(error.rawString())\n                    return false\n                }\n            }\n        done.drive(onNext: { [weak self] fields in\n            // 保存设置\n            self?.dependencies.settingFieldRelay.accept(fields)\n        }).disposed(by: rx.disposeBag)\n\n        let copyScript = input.copyScript\n            .filter { [weak self] fields in\n                do {\n                    _ = try AESCryptoModel(cryptoFields: fields)\n                    // 保存配置\n                    self?.dependencies.settingFieldRelay.accept(fields)\n                    return true\n                } catch {\n                    showSnackbar.accept(error.rawString())\n                    return false\n                }\n            }\n        let copy = Driver.combineLatest(copyScript, dependencies.deviceKey, dependencies.serverAddress)\n            .compactMap { fields, deviceKey, serverAddress -> String? in\n                let key = fields.key ?? \"\"\n                let iv = fields.iv ?? \"\"\n                if fields.mode == \"GCM\" {\n                    return\n                        \"\"\"\n                        // Documentation: \\(\"encryptionUrl\".localized)\n                        \n                        const crypto = require('crypto');\n\n\n                        // bark key\n                        const deviceKey = '\\(deviceKey)';\n                        // push payload\n                        const json = JSON.stringify({ body: \"test\", sound: \"birdsong\" });\n\n                        // \\(\"keyComment\".localized(with: Int(fields.algorithm.suffix(3))! / 8))\n                        const key = '\\(key)';\n                        // \\(\"ivComment\".localized)\n                        const iv = '\\(iv)';\n\n                        // AES-\\(fields.algorithm.suffix(3))-GCM\n                        const cipher = crypto.createCipheriv('aes-\\(fields.algorithm.suffix(3))-gcm', Buffer.from(key, 'utf8'), Buffer.from(iv, 'utf8'));\n                        const encrypted = Buffer.concat([\n                          cipher.update(json, 'utf8'),\n                          cipher.final()\n                        ]);\n                        const tag = cipher.getAuthTag()\n                        \n                        const combined = Buffer.concat([encrypted, tag])\n                        let ciphertext = combined.toString('base64')\n\n                        // \\(\"consoleComment\".localized) \"\\((try? AESCryptoModel(cryptoFields: fields).encrypt(text: \"{\\\"body\\\":\\\"test\\\",\\\"sound\\\":\\\"birdsong\\\"}\")) ?? \"\")\"\n                        console.log(ciphertext);\n\n                        // \\(\"ciphertextComment\".localized)\n                        const pushUrl = `\\(serverAddress)/${deviceKey}?ciphertext=${encodeURIComponent(ciphertext)}&iv=${encodeURIComponent(iv)}`;\n                        \"\"\"\n                } else {\n                    return\n                        \"\"\"\n                        #!/usr/bin/env bash\n                        \n                        # Documentation: \\(\"encryptionUrl\".localized)\n                        \n                        set -e\n\n                        # bark key\n                        deviceKey='\\(deviceKey)'\n                        # push payload\n                        json='{\"body\": \"test\", \"sound\": \"birdsong\"}'\n\n                        # \\(\"keyComment\".localized(with: Int(fields.algorithm.suffix(3))! / 8)) )\n                        key='\\(key)'\n                        # \\(\"ivComment\".localized)\n                        iv='\\(iv)'\n\n                        # \\(\"opensslEncodingComment\".localized)\n                        key=$(printf $key | xxd -ps -c 200)\n                        iv=$(printf $iv | xxd -ps -c 200)\n                        \n                        # \\(\"base64Notice\".localized)\n                        ciphertext=$(echo -n $json | openssl enc -aes-\\(fields.algorithm.suffix(3))-\\(fields.mode.lowercased()) -K $key \\(iv.count > 0 ? \"-iv $iv \" : \"\")| base64)\n\n                        # \\(\"consoleComment\".localized) \"\\((try? AESCryptoModel(cryptoFields: fields).encrypt(text: \"{\\\"body\\\": \\\"test\\\", \\\"sound\\\": \\\"birdsong\\\"}\")) ?? \"\")\"\n                        echo $ciphertext\n                        \n                        # \\(\"ciphertextComment\".localized)\n                        curl --data-urlencode \"ciphertext=$ciphertext\"\\(iv.count == 0 ? \"\" : \" --data-urlencode \\\"iv=\\(iv)\\\"\") \\(serverAddress)/$deviceKey\n                        \"\"\"\n                }\n            }\n\n        return Output(\n            initial: Driver.just((\n                algorithmList: [Algorithm.aes128, Algorithm.aes192, Algorithm.aes256],\n                modeList: [\"CBC\", \"ECB\", \"GCM\"],\n                paddingList: [\"pkcs7\"],\n                initialFields: dependencies.settingFieldRelay.value\n            )),\n            modeListChanged: modeList,\n            paddingListChanged: paddingList,\n            keyLengthChanged: keyLength,\n            showSnackbar: showSnackbar.asDriver(onErrorDriveWith: .empty()),\n            done: done.map { _ in () },\n            copy: copy\n        )\n    }\n}\n"
  },
  {
    "path": "Controller/HomeViewController.swift",
    "content": "//\n//  HomeViewController.swift\n//  Bark\n//\n//  Created by huangfeng on 2018/3/7.\n//  Copyright © 2018年 Fin. All rights reserved.\n//\n\nimport Material\nimport RxCocoa\nimport RxDataSources\nimport RxSwift\nimport UIKit\nimport UserNotifications\n\nclass HomeViewController: BaseViewController<HomeViewModel> {\n    let newButton: BKButton = {\n        let btn = BKButton()\n        btn.setImage(Icon.add, for: .normal)\n        btn.imageView?.tintColor = BKColor.grey.darken4\n        btn.frame = CGRect(x: 0, y: 0, width: 40, height: 40)\n        btn.accessibilityIdentifier = \"AddServer\".localized\n        return btn\n    }()\n    \n    let serversButton: BKButton = {\n        let btn = BKButton()\n        btn.setImage(UIImage(named: \"baseline_filter_drama_black_24pt\"), for: .normal)\n        btn.imageView?.tintColor = BKColor.grey.darken4\n        btn.frame = CGRect(x: 0, y: 0, width: 40, height: 40)\n        btn.accessibilityIdentifier = \"serverList\".localized\n        return btn\n    }()\n    \n    let startButton: FABButton = {\n        let button = FABButton(title: \"RegisterDevice\".localized)\n        button.backgroundColor = BKColor.grey.lighten5\n        button.transition([.scale(0.75), .opacity(0)])\n        return button\n    }()\n        \n    let tableView: UITableView = {\n        let tableView = UITableView()\n        tableView.separatorStyle = .none\n        tableView.backgroundColor = BKColor.background.primary\n        tableView.register(PreviewCardCell.self, forCellReuseIdentifier: \"\\(PreviewCardCell.self)\")\n        tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 20, right: 0)\n        return tableView\n    }()\n    \n    override func makeUI() {\n        self.view.backgroundColor = BKColor.background.primary\n        \n        navigationItem.setBarButtonItems(items: [\n            UIBarButtonItem(customView: newButton),\n            UIBarButtonItem(customView: serversButton)\n        ], position: .right)\n        \n        self.view.addSubview(self.tableView)\n        self.tableView.snp.makeConstraints { make in\n            make.top.right.bottom.left.equalToSuperview()\n        }\n        \n        self.view.addSubview(self.startButton)\n        self.startButton.snp.makeConstraints { make in\n            make.width.height.equalTo(150)\n            make.centerX.equalToSuperview()\n            make.centerY.equalToSuperview().offset(-50)\n        }\n        \n        Client.shared.currentTabBarController?\n            .tabBarItemDidClick\n            .filter { $0 == .service }\n            .subscribe(onNext: { [weak self] _ in\n                self?.tableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: .top, animated: true)\n            }).disposed(by: self.rx.disposeBag)\n    }\n\n    override func bindViewModel() {\n        // 第一次进入APP 查看通知权限设置\n        let authorizationStatus = Single<UNAuthorizationStatus>.create { single -> Disposable in\n            UNUserNotificationCenter.current().getNotificationSettings { settings in\n                single(.success(settings.authorizationStatus))\n            }\n            return Disposables.create()\n        }\n        \n        // 请求通知权限操作\n        let startRequestAuthorization: () -> Observable<Bool> = {\n            Single<Bool>.create { single -> Disposable in\n                let center = UNUserNotificationCenter.current()\n                center.requestAuthorization(options: [.alert, .sound, .badge, .criticalAlert], completionHandler: { (_ granted: Bool, _: Error?) in\n                    single(.success(granted))\n                })\n                return Disposables.create()\n            }\n            .asObservable()\n        }\n        \n        let output = viewModel.transform(\n            input: HomeViewModel.Input(\n                addCustomServerTap: newButton.rx.tap.asDriver(),\n                serverListTap: serversButton.rx.tap.asDriver(),\n                viewDidAppear: self.rx.methodInvoked(#selector(viewDidAppear(_:)))\n                    .map { _ in () }\n                    .asDriver(onErrorDriveWith: .empty()),\n                start: self.startButton.rx.tap.asDriver(),\n                clientState: Client.shared.state.asDriver(),\n                authorizationStatus: authorizationStatus,\n                startRequestAuthorizationCreator: startRequestAuthorization\n            )\n        )\n        \n        let dataSource = RxTableViewSectionedReloadDataSource<SectionModel<String, PreviewCardCellViewModel>> { _, tableView, _, item -> UITableViewCell in\n            if let cell = tableView.dequeueReusableCell(withIdentifier: \"\\(PreviewCardCell.self)\") as? PreviewCardCell {\n                cell.bindViewModel(model: item)\n                return cell\n            }\n            return UITableViewCell()\n        }\n        \n        // 标题\n        output.title\n            .drive(self.navigationItem.rx.title)\n            .disposed(by: rx.disposeBag)\n        \n        // TableView数据源\n        output.previews\n            .drive(self.tableView.rx.items(dataSource: dataSource))\n            .disposed(by: rx.disposeBag)\n        \n        // 跳转到对应页面\n        output.push\n            .drive(onNext: { [weak self] viewModel in\n                self?.pushViewModel(viewModel: viewModel)\n            })\n            .disposed(by: rx.disposeBag)\n        output.present\n            .drive(onNext: { [weak self] viewModel in\n                self?.presentViewModel(viewModel: viewModel)\n            })\n            .disposed(by: rx.disposeBag)\n        \n        // 通过ping服务器，判断 clienState\n        output.clienStateChanged\n            .drive(Client.shared.state)\n            .disposed(by: rx.disposeBag)\n        \n        // 根据通知权限，设置是否隐藏注册按钮、显示示例预览列表\n        output.tableViewHidden\n            .map { !$0 }\n            .drive(self.tableView.rx.isHidden)\n            .disposed(by: rx.disposeBag)\n        output.tableViewHidden\n            .drive(self.startButton.rx.isHidden)\n            .disposed(by: rx.disposeBag)\n        \n        // 注册推送\n        output.registerForRemoteNotifications.drive(onNext: {\n            UIApplication.shared.registerForRemoteNotifications()\n        })\n        .disposed(by: rx.disposeBag)\n        \n        // 弹出提示\n        output.showSnackbar\n            .drive(onNext: { [weak self] text in\n                self?.showSnackbar(text: text)\n            })\n            .disposed(by: rx.disposeBag)\n        \n        // 弹出服务器错误提示，引导用户跳转FAQ\n        output.alertServerError\n            .drive(onNext: { [weak self] error in\n                self?.alertServerError(error: error)\n            })\n            .disposed(by: rx.disposeBag)\n        \n        // startButton是否可点击\n        output.startButtonEnable\n            .drive(self.startButton.rx.isEnabled)\n            .disposed(by: rx.disposeBag)\n        \n        // 复制文本\n        output.copy\n            .drive(onNext: { [weak self] text in\n                UIPasteboard.general.string = text\n                self?.showSnackbar(text: \"Copy\".localized)\n            })\n            .disposed(by: rx.disposeBag)\n        \n        // 预览\n        output.preview\n            .drive(onNext: { url in\n                UIApplication.shared.open(url, options: [:], completionHandler: nil)\n            })\n            .disposed(by: rx.disposeBag)\n        \n        // 原样刷新 TableView\n        output.reloadData\n            .drive(onNext: { [weak self] in\n                self?.tableView.reloadData()\n            })\n            .disposed(by: rx.disposeBag)\n    }\n    \n    func pushViewModel(viewModel: ViewModel) {\n        var viewController: UIViewController?\n        if let viewModel = viewModel as? NewServerViewModel {\n            viewController = NewServerViewController(viewModel: viewModel)\n        } else if let viewModel = viewModel as? SoundsViewModel {\n            viewController = SoundsViewController(viewModel: viewModel)\n        } else if let viewModel = viewModel as? CryptoSettingViewModel {\n            self.navigationController?.present(BarkNavigationController(rootViewController: CryptoSettingController(viewModel: viewModel)), animated: true)\n            return\n        }\n        if let viewController = viewController {\n            self.navigationController?.pushViewController(viewController, animated: true)\n        }\n    }\n    \n    func presentViewModel(viewModel: ViewModel) {\n        if let viewModel = viewModel as? ServerListViewModel {\n            let controller = BarkSnackbarController(\n                rootViewController: BarkNavigationController(\n                    rootViewController: ServerListViewController(viewModel: viewModel)))\n            self.navigationController?.present(controller, animated: true, completion: nil)\n        }\n    }\n    \n    func alertServerError(error: String) {\n        let alertController = UIAlertController(title: \"ServerError\".localized, message: error, preferredStyle: .alert)\n        alertController.addAction(UIAlertAction(title: \"faq\".localized, style: .default, handler: { [weak self] _ in\n            guard let url = try? \"faqUrl\".localized.asURL() else {\n                return\n            }\n            self?.navigationController?.present(BarkSFSafariViewController(url: url), animated: true, completion: nil)\n        }))\n        alertController.addAction(UIAlertAction(title: \"Cancel\".localized, style: .cancel, handler: nil))\n        self.present(alertController, animated: true, completion: nil)\n    }\n}\n"
  },
  {
    "path": "Controller/HomeViewModel.swift",
    "content": "//\n//  HomeViewModel.swift\n//  Bark\n//\n//  Created by huangfeng on 2020/11/18.\n//  Copyright © 2020 Fin. All rights reserved.\n//\n\nimport Foundation\nimport RxCocoa\nimport RxDataSources\nimport RxSwift\nimport SwiftyJSON\nimport UserNotifications\n\nclass HomeViewModel: ViewModel, ViewModelType {\n    struct Input {\n        let addCustomServerTap: Driver<Void>\n        let serverListTap: Driver<Void>\n        let viewDidAppear: Driver<Void>\n        let start: Driver<Void>\n        let clientState: Driver<Client.ClienState>\n        let authorizationStatus: Single<UNAuthorizationStatus>\n        let startRequestAuthorizationCreator: () -> Observable<Bool>\n    }\n\n    struct Output {\n        let previews: Driver<[SectionModel<String, PreviewCardCellViewModel>]>\n        let push: Driver<ViewModel>\n        let present: Driver<ViewModel>\n        let title: Driver<String>\n        let clienStateChanged: Driver<Client.ClienState>\n        let tableViewHidden: Driver<Bool>\n        let showSnackbar: Driver<String>\n        let alertServerError: Driver<String>\n        let startButtonEnable: Driver<Bool>\n        let copy: Driver<String>\n        let preview: Driver<URL>\n        let reloadData: Driver<Void>\n        let registerForRemoteNotifications: Driver<Void>\n    }\n    \n    let previews: [PreviewModel] = [\n        PreviewModel(\n            body: \"CustomedNotificationContent\".localized,\n            notice: \"Notice1\".localized\n        ),\n        PreviewModel(\n            title: \"CustomedNotificationTitle\".localized,\n            body: \"CustomedNotificationContent\".localized,\n            notice: \"Notice2\".localized\n        ),\n        PreviewModel(\n            body: \"notificationSound\".localized,\n            notice: \"setSounds\".localized,\n            queryParameter: \"sound=minuet\",\n            moreInfo: \"viewAllSounds\".localized,\n            moreViewModel: SoundsViewModel()\n        ),\n        PreviewModel(\n            body: \"ringtone\".localized,\n            notice: \"ringtoneNotice\".localized,\n            queryParameter: \"call=1\"\n        ),\n        PreviewModel(\n            body: \"archiveNotificationMessageTitle\".localized,\n            notice: \"archiveNotificationMessage\".localized,\n            queryParameter: \"isArchive=1\"\n        ),\n        PreviewModel(\n            body: \"notificationIcon\".localized,\n            notice: \"notificationIconNotice\".localized,\n            queryParameter: \"icon=https://day.app/assets/images/avatar.jpg\",\n            image: UIImage(named: \"icon\")\n        ),\n        PreviewModel(\n            body: \"messageGroup\".localized,\n            notice: \"groupMessagesNotice\".localized,\n            queryParameter: \"group=groupName\",\n            image: UIImage(named: \"group\")\n        ),\n        PreviewModel(\n            body: \"pushNotificationEncryption\".localized,\n            notice: \"encryptionNotice\".localized,\n            queryParameter: \"ciphertext=ciphertext\",\n            moreInfo: \"encryptionSettings\".localized,\n            moreViewModel: CryptoSettingViewModel()\n        ),\n        PreviewModel(\n            body: \"criticalAlert\".localized,\n            notice: \"criticalAlertNotice\".localized,\n            queryParameter: \"level=critical&volume=5\",\n            image: UIImage(named: \"criticalAlert\")\n        ),\n        PreviewModel(\n            body: \"interruptionLevel\".localized,\n            notice: \"interruptionLevelNotice\".localized,\n            queryParameter: \"level=timeSensitive\"\n        ),\n        PreviewModel(\n            body: \"URL Test\",\n            notice: \"urlParameter\".localized,\n            queryParameter: \"url=https://www.baidu.com\"\n        ),\n        PreviewModel(\n            body: \"imagePushNotification\".localized,\n            notice: \"imageParameter\".localized,\n            queryParameter: \"image=https://day.app/assets/images/avatar.jpg\"\n        ),\n        PreviewModel(\n            body: \"Copy Test\",\n            notice: \"copyParameter\".localized,\n            queryParameter: \"copy=test\",\n            image: UIImage(named: \"copyTest\")\n        ),\n        PreviewModel(\n            body: \"badge\".localized,\n            notice: \"badgeNotice\".localized,\n            queryParameter: \"badge=1\"\n        ),\n        PreviewModel(\n            body: \"automaticallyCopyTitle\".localized,\n            notice: \"automaticallyCopy\".localized,\n            queryParameter: \"autoCopy=1&copy=optional\"\n        )\n    ]\n    \n    /// 记录服务器错误的次数，如果错误次数大于2次，弹出提示引导用户查看FAQ。\n    private var serverErrorCount = 0\n    \n    func transform(input: Input) -> Output {\n        let title = BehaviorRelay(value: ServerManager.shared.currentServer.host)\n        \n        let sectionModel = SectionModel(\n            model: \"previews\",\n            items: previews.map { PreviewCardCellViewModel(previewModel: $0, clientState: input.clientState) }\n        )\n        \n        // 点击跳转到添加自定义服务器\n        let customServer = input.addCustomServerTap.map { NewServerViewModel() as ViewModel }\n        \n        // 如果更改了服务器地址，返回时也需更改 title\n        customServer\n            .flatMapLatest { model -> Driver<String> in\n                (model as! NewServerViewModel).pop.asDriver(onErrorJustReturn: \"\")\n            }\n            .drive(title)\n            .disposed(by: rx.disposeBag)\n\n        // 点击preview中的notice ，跳转到对应的页面\n        let noticeTap = Driver.merge(sectionModel.items.map { $0.noticeTap.asDriver(onErrorDriveWith: .empty()) })\n        \n        // 判断服务器状态\n        let clienState = input.viewDidAppear\n            .asObservable().flatMapLatest { _ -> Observable<Result<JSON, ApiError>> in\n                BarkApi.provider\n                    .request(.ping(baseURL: ServerManager.shared.currentServer.address))\n                    .filterResponseError()\n            }\n            .map { response -> Client.ClienState in\n                switch response {\n                case .failure(let error):\n                    return .serverError(error: error)\n                default:\n                    return .ok\n                }\n            }\n        \n        // 根据通知权限，设置是否隐藏注册按钮、显示示例预览列表\n        let tableViewHidden = input.authorizationStatus.map { $0 == .authorized }\n            .asObservable()\n            .concat(\n                input.start.asObservable().flatMapLatest { input.startRequestAuthorizationCreator() }\n            )\n            .asDriver(onErrorJustReturn: false)\n        \n        let showSnackbar = PublishRelay<String>()\n        let alertServerError = PublishRelay<String>()\n        \n        // 点击注册按钮后，如果不允许推送，弹出提示\n        tableViewHidden\n            .skip(1)\n            .compactMap { granted -> String? in\n                if !granted {\n                    return \"AllowNotifications\".localized\n                }\n                return nil\n            }\n            .asObservable()\n            .bind(to: showSnackbar)\n            .disposed(by: rx.disposeBag)\n        \n        // 点击注册按钮，如果用户允许推送，则通知 viewController 注册推送\n        let registerForRemoteNotifications = tableViewHidden\n            .skip(1)\n            .filter { $0 }\n            .map { _ in () }\n\n        // client state 变化时，发出相应错误提醒\n        input.clientState.drive(onNext: { [weak self] state in\n            guard let self else { return }\n            \n            switch state {\n            case .ok: break\n            case .serverError(let error):\n                if serverErrorCount < 2 {\n                    showSnackbar.accept(\"\\(\"ServerError\".localized): \\(error.rawString())\")\n                } else {\n                    alertServerError.accept(error.rawString())\n                }\n                serverErrorCount += 1\n            default: break\n            }\n            // 主要用于 url scheme 添加服务器时会有state状态改变事件，顺便更新下标题\n            title.accept(ServerManager.shared.currentServer.host)\n        })\n        .disposed(by: rx.disposeBag)\n        \n        let serverList = input.serverListTap.map { ServerListViewModel() as ViewModel }\n        \n        // 服务器发生了改变\n        serverList\n            .flatMapLatest { model -> Driver<Server> in\n                (model as! ServerListViewModel).currentServerChanged.asDriver(onErrorDriveWith: .empty())\n            }\n            .map { server -> String in\n                server.host\n            }\n            .drive(title)\n            .disposed(by: rx.disposeBag)\n     \n        return Output(\n            previews: Driver.just([sectionModel]),\n            push: Driver<ViewModel>.merge(customServer, noticeTap),\n            present: serverList.asDriver(),\n            title: title.asDriver(),\n            clienStateChanged: clienState.asDriver(onErrorDriveWith: .empty()),\n            tableViewHidden: tableViewHidden,\n            showSnackbar: showSnackbar.asDriver(onErrorDriveWith: .empty()),\n            alertServerError: alertServerError.asDriver(onErrorDriveWith: .empty()),\n            startButtonEnable: Driver.just(true),\n            copy: Driver.merge(sectionModel.items.map { $0.copy.asDriver(onErrorDriveWith: .empty()) }),\n            preview: Driver.merge(sectionModel.items.map { $0.preview.asDriver(onErrorDriveWith: .empty()) }),\n            reloadData: input.clientState.map { _ in () },\n            registerForRemoteNotifications: registerForRemoteNotifications\n        )\n    }\n}\n"
  },
  {
    "path": "Controller/MessageListViewController.swift",
    "content": "//\n//  MessageListViewController.swift\n//  Bark\n//\n//  Created by huangfeng on 2020/5/25.\n//  Copyright © 2020 Fin. All rights reserved.\n//\n\nimport Material\nimport MJRefresh\nimport RealmSwift\nimport RxCocoa\nimport RxDataSources\nimport RxSwift\nimport UIKit\nimport UniformTypeIdentifiers\n\nclass MessageListViewController: BaseViewController<MessageListViewModel> {\n    lazy var deleteButton: UIBarButtonItem = {\n        if #available(iOS 14.0, *) {\n            var menuElements = [UIMenuElement]()\n            for range in [MessageDeleteTimeRange.lastHour, .today, .todayAndYesterday, .lastMonth, .allTime] {\n                let action = UIAction(title: range.string) { [weak self] _ in\n                    self?.clearAlert(range)\n                }\n                menuElements.append(action)\n            }\n            \n            var subMenuElements = [UIMenuElement]()\n            for range in [MessageDeleteTimeRange.beforeOneHour, .beforeToday, .beforeYesterday, .beforeOneMonth] {\n                let action = UIAction(title: range.string) { [weak self] _ in\n                    self?.clearAlert(range)\n                }\n                subMenuElements.append(action)\n            }\n            menuElements.append(UIMenu(title: \"more\".localized, children: subMenuElements))\n\n            let addNewMenu = UIMenu(\n                title: \"clearFrom\".localized,\n                children: menuElements\n            )\n            let item = UIBarButtonItem(image: UIImage(named: \"baseline_delete_outline_black_24pt\"), menu: addNewMenu)\n            item.accessibilityLabel = \"clear\".localized\n            return item\n        } else {\n            let btn = BKButton()\n            btn.setImage(UIImage(named: \"baseline_delete_outline_black_24pt\"), for: .normal)\n            btn.frame = CGRect(x: 0, y: 0, width: 40, height: 40)\n            btn.accessibilityLabel = \"clear\".localized\n            return UIBarButtonItem(customView: btn)\n        }\n        \n    }()\n    \n    let groupButton: UIBarButtonItem = {\n        let btn = BKButton()\n        btn.setImage(UIImage(named: \"group_expand\")?.withRenderingMode(.alwaysTemplate), for: .normal)\n        btn.setImage(UIImage(named: \"group_collapse\")?.withRenderingMode(.alwaysTemplate), for: .selected)\n        btn.imageView?.tintColor = BKColor.black\n        btn.frame = CGRect(x: 0, y: 0, width: 40, height: 40)\n        btn.accessibilityLabel = \"toggle\".localized\n        return UIBarButtonItem(customView: btn)\n    }()\n    \n    lazy var tableView: UITableView = {\n        let tableView = UITableView()\n        tableView.separatorStyle = .none\n        tableView.backgroundColor = BKColor.background.primary\n        tableView.register(MessageTableViewCell.self, forCellReuseIdentifier: \"\\(MessageTableViewCell.self)\")\n        tableView.register(MessageGroupTableViewCell.self, forCellReuseIdentifier: \"\\(MessageGroupTableViewCell.self)\")\n        // 设置了这个后，第一次进页面 LargeTitle 就会收缩成小标题，不设置这个LargeTitle就是大标题显示\n        // 谁特么能整的明白这个？\n        // tableView.contentInset = UIEdgeInsets(top: 20, left: 0, bottom: 0, right: 0)\n        \n        // 替代 contentInset 设置一个 header\n        tableView.tableHeaderView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: 20))\n        \n        tableView.rx.setDelegate(self).disposed(by: rx.disposeBag)\n        tableView.mj_footer = MJRefreshAutoFooter()\n        \n        return tableView\n    }()\n    \n    /// 展开的群组\n    private var expandedGroup: Set<String> = []\n    /// 下拉刷新标记字段\n    private var canRefresh = true\n    \n    /// 群组中删除消息的事件流\n    private let itemDeleteInGroupRelay = PublishRelay<MessageItemModel>()\n    /// 下拉刷新事件流\n    private let refreshRelay = PublishRelay<Void>()\n    /// 重新刷新已加载的页的数据 （最多10页）\n    private let reloadRelay = PublishRelay<Void>()\n    /// 按时间范围清除消息事件流\n    private let clearRelay = PublishRelay<MessageDeleteTimeRange>()\n\n    override func makeUI() {\n        navigationItem.searchController = UISearchController(searchResultsController: nil)\n        navigationItem.searchController?.obscuresBackgroundDuringPresentation = false\n        navigationItem.searchController?.delegate = self\n        \n        if #available(iOS 26.0, *) {\n            navigationItem.preferredSearchBarPlacement = .integratedButton\n        }\n        \n        self.view.addSubview(tableView)\n        tableView.snp.makeConstraints { make in\n            make.edges.equalToSuperview()\n        }\n\n        if #available(iOS 14.0, *) {\n            // iOS 14 以上，使用 UIMenu\n        } else {\n            // 使用 UIAlertController\n            subscribeDeleteTap()\n        }\n        \n        // 点击tab按钮，回到顶部\n        Client.shared.currentTabBarController?\n            .tabBarItemDidClick\n            .filter { $0 == .messageHistory }\n            .subscribe(onNext: { [weak self] _ in\n                self?.scrollToTop()\n            }).disposed(by: self.rx.disposeBag)\n        \n        NotificationCenter.default.rx\n            .notification(UIApplication.willEnterForegroundNotification)\n            .delay(.milliseconds(500), scheduler: MainScheduler.instance) // 延迟0.5秒，等待数据库 Results 更新到最新数据集\n            .subscribe(onNext: { [weak self] _ in\n                self?.reloadRelay.accept(())\n            }).disposed(by: rx.disposeBag)\n        \n        // 点击群组消息，展开群\n        tableView.rx.itemSelected.subscribe(onNext: { [weak self] indexPath in\n            guard let self else { return }\n            if let cell = self.tableView.cellForRow(at: indexPath) as? MessageGroupTableViewCell {\n                if !cell.isExpanded {\n                    UIView.animate(withDuration: 0.6, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.2) {\n                        self.tableView.performBatchUpdates {\n                            cell.isExpanded = true\n                        }\n                    }\n                    if let groupName = cell.cellData?.groupName {\n                        self.expandedGroup.insert(groupName)\n                    }\n                }\n            }\n        }).disposed(by: rx.disposeBag)\n    }\n\n    // tableView 数据源\n    private lazy var dataSource = RxTableViewSectionedAnimatedDataSource<MessageSection>(\n        animationConfiguration: AnimationConfiguration(\n            insertAnimation: .none,\n            reloadAnimation: .none,\n            deleteAnimation: .left\n        ),\n        configureCell: { [weak self] _, tableView, _, item -> UITableViewCell in\n            guard let self else { return UITableViewCell() }\n            \n            switch item {\n            case .message(let message):\n                guard let cell = tableView.dequeueReusableCell(withIdentifier: \"\\(MessageTableViewCell.self)\") as? MessageTableViewCell else {\n                    return UITableViewCell()\n                }\n                cell.tapAction = { [weak self, weak cell] message, sourceView in\n                    guard let self, let cell else { return }\n                    self.alertMessage(message: message, sourceView: sourceView, sourceCell: cell)\n                }\n                cell.message = message\n                return cell\n            case .messageGroup(let title, let totalCount, let messages):\n                guard let cell = tableView.dequeueReusableCell(withIdentifier: \"\\(MessageGroupTableViewCell.self)\") as? MessageGroupTableViewCell else {\n                    return UITableViewCell()\n                }\n                cell.showLessAction = { [weak self, weak cell] in\n                    guard let self else { return }\n                    UIView.animate(withDuration: 0.6, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.2) {\n                        self.tableView.performBatchUpdates {\n                            cell?.isExpanded = false\n                        }\n                        if let groupName = cell?.cellData?.groupName {\n                            self.expandedGroup.remove(groupName)\n                        }\n                    }\n                }\n                cell.showGroupMessageAction = { [weak self] group in\n                    let viewModel = MessageListViewModel(sourceType: .group(group))\n                    let controller = MessageListViewController(viewModel: viewModel)\n                    self?.navigationController?.pushViewController(controller, animated: true)\n                }\n                cell.clearAction = { [weak self, weak cell] in\n                    guard let self, let cell, let indexPath = self.tableView.indexPath(for: cell) else { return }\n                    self.tableView.dataSource?.tableView?(self.tableView, commit: .delete, forRowAt: indexPath)\n                }\n                cell.tapAction = { [weak self, weak cell] message, sourceView in\n                    guard let self, let cell else { return }\n                    self.alertMessage(message: message, sourceView: sourceView, sourceCell: cell)\n                }\n                cell.cellData = (title, totalCount, messages)\n                cell.isExpanded = self.expandedGroup.contains(title)\n                return cell\n            }\n\n        }, canEditRowAtIndexPath: { _, _ in\n            true\n        }\n    )\n    \n    override func bindViewModel() {\n        guard let groupBtn = groupButton.customView as? BKButton else {\n            return\n        }\n        \n        let output = viewModel.transform(\n            input: MessageListViewModel.Input(\n                refresh: refreshRelay.asDriver(onErrorDriveWith: .empty()),\n                loadMore: tableView.mj_footer!.rx.refresh.asDriver(),\n                itemDelete: tableView.rx.modelDeleted(MessageListCellItem.self).asDriver(),\n                itemDeleteInGroup: itemDeleteInGroupRelay.asDriver(onErrorDriveWith: .empty()),\n                delete: clearRelay.asDriver(onErrorDriveWith: .empty()),\n                groupToggleTap: groupBtn.rx.tap.asDriver(),\n                searchText: navigationItem.searchController!.searchBar.rx.text.asObservable(),\n                reload: reloadRelay.asDriver(onErrorDriveWith: .empty())\n            ))\n        \n        // tableView 刷新状态\n        output.refreshAction\n            .drive(tableView.rx.refreshAction)\n            .disposed(by: rx.disposeBag)\n        \n        output.messages\n            .drive(tableView.rx.items(dataSource: dataSource))\n            .disposed(by: rx.disposeBag)\n        \n        // 选择群组\n        output.type\n            .drive(onNext: { [weak self] type in\n                (self?.groupButton.customView as? UIButton)?.isSelected = type == .group\n            }).disposed(by: rx.disposeBag)\n        \n        // 标题\n        output.title\n            .drive(self.navigationItem.rx.title).disposed(by: rx.disposeBag)\n        \n        // 数据库初始化出错错误提示\n        output.errorAlert\n            .drive(onNext: { [weak self] error in\n                let alertController = UIAlertController(title: \"Error\", message: error, preferredStyle: .alert)\n                alertController.addAction(UIAlertAction(title: \"Copy2\".localized, style: .default, handler: { _ in\n                    UIPasteboard.general.string = error\n                }))\n                alertController.addAction(UIAlertAction(title: \"Cancel\".localized, style: .cancel, handler: nil))\n                self?.present(alertController, animated: true, completion: nil)\n            }).disposed(by: rx.disposeBag)\n        \n        output.groupToggleButtonHidden\n            .drive(onNext: { [weak self] isHidden in\n                guard let self else { return }\n                var items = [deleteButton]\n                if !isHidden {\n                    items.append(groupButton)\n                }\n                navigationItem.setBarButtonItems(items: items, position: .right)\n            }).disposed(by: rx.disposeBag)\n    }\n    \n    private func subscribeDeleteTap() {\n        guard let deleteBtn = deleteButton.customView as? BKButton else {\n            return\n        }\n        deleteBtn.rx.tap.subscribe(onNext: { [weak self] _ in\n            guard let self else { return }\n            \n            let alertController = UIAlertController(title: nil, message: \"clearFrom\".localized, preferredStyle: .actionSheet)\n            alertController.addAction(UIAlertAction(title: \"lastHour\".localized, style: .default, handler: { [weak self] _ in\n                self?.clearAlert(.lastHour)\n            }))\n            alertController.addAction(UIAlertAction(title: \"today\".localized, style: .default, handler: { [weak self] _ in\n                self?.clearAlert(.today)\n            }))\n            alertController.addAction(UIAlertAction(title: \"todayAndYesterday\".localized, style: .default, handler: { [weak self] _ in\n                self?.clearAlert(.todayAndYesterday)\n            }))\n            alertController.addAction(UIAlertAction(title: \"allTime\".localized, style: .default, handler: { [weak self] _ in\n                self?.clearAlert(.allTime)\n            }))\n            alertController.addAction(UIAlertAction(title: \"Cancel\".localized, style: .cancel, handler: nil))\n            if UIDevice.current.userInterfaceIdiom == .pad {\n                alertController.modalPresentationStyle = .popover\n                if #available(iOS 16.0, *) {\n                    alertController.popoverPresentationController?.sourceItem = self.deleteButton\n                } else {\n                    alertController.popoverPresentationController?.barButtonItem = self.deleteButton\n                }\n            }\n            self.navigationController?.present(alertController, animated: true, completion: nil)\n        }).disposed(by: rx.disposeBag)\n    }\n    \n    func clearAlert(_ range: MessageDeleteTimeRange) {\n        let alertController = UIAlertController(title: nil, message: \"\\(\"clearFrom\".localized)\\n\\(range.string)\", preferredStyle: .alert)\n        alertController.addAction(UIAlertAction(title: \"clear\".localized, style: .destructive, handler: { [weak self] _ in\n            self?.clearRelay.accept(range)\n        }))\n        alertController.addAction(UIAlertAction(title: \"Cancel\".localized, style: .cancel, handler: nil))\n        self.navigationController?.present(alertController, animated: true, completion: nil)\n    }\n    \n    private func alertMessage(message: MessageItemModel, sourceView: MessageItemView, sourceCell: UITableViewCell) {\n        let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)\n        \n        // 复制\n        alertController.addAction(UIAlertAction(title: \"Copy2\".localized, style: .default, handler: { [weak self]\n            (_: UIAlertAction) in\n                if #available(iOS 14.0, *) {\n                    var items = [[String: Any]]()\n                    items.append([UTType.utf8PlainText.identifier: message.attributedText?.string ?? \"\"])\n                    if let image = sourceView.imageView.image {\n                        items.append([UTType.image.identifier: image])\n                    }\n                    UIPasteboard.general.items = items\n                } else {\n                    UIPasteboard.general.string = message.attributedText?.string ?? \"\"\n                }\n                self?.showSnackbar(text: \"Copy\".localized)\n        }))\n        // 删除\n        alertController.addAction(UIAlertAction(title: \"removeMessage\".localized, style: .destructive, handler: { [weak self]\n            (_: UIAlertAction) in\n                guard let self, let indexPath = self.tableView.indexPath(for: sourceCell) else { return }\n                if sourceCell is MessageTableViewCell {\n                    // 单个消息，把cell删除\n                    self.tableView.dataSource?.tableView?(self.tableView, commit: .delete, forRowAt: indexPath)\n                } else if sourceCell is MessageGroupTableViewCell {\n                    // 群组消息，只能删除群组中需删除的消息\n                    self.itemDeleteInGroupRelay.accept(message)\n                }\n        }))\n        // 取消\n        alertController.addAction(UIAlertAction(title: \"Cancel\".localized, style: .cancel, handler: { _ in }))\n        \n        if UIDevice.current.userInterfaceIdiom == .pad {\n            alertController.popoverPresentationController?.sourceView = sourceView.superview\n            alertController.popoverPresentationController?.sourceRect = sourceView.frame\n            alertController.modalPresentationStyle = .popover\n        }\n        \n        self.navigationController?.present(alertController, animated: true, completion: nil)\n    }\n    \n    private func scrollToTop() {\n        if self.tableView.visibleCells.count > 0 {\n            self.tableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: .top, animated: true)\n        }\n    }\n}\n\nextension MessageListViewController: UITableViewDelegate {\n    func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {\n        let action = UIContextualAction(style: .destructive, title: \"removeMessage\".localized) { [weak self] _, _, actionPerformed in\n            guard let self else { return }\n            \n            if self.tableView.cellForRow(at: indexPath) is MessageTableViewCell {\n                // 单个消息直接删除，不弹出提示\n                self.tableView.dataSource?.tableView?(self.tableView, commit: .delete, forRowAt: indexPath)\n                actionPerformed(true)\n                return\n            }\n            \n            // 群组消息删除，弹出个确认提示\n            let alertView = UIAlertController(title: nil, message: \"removeNotice\".localized, preferredStyle: .alert)\n            alertView.addAction(UIAlertAction(title: \"removeMessage\".localized, style: .destructive, handler: { _ in\n                self.tableView.dataSource?.tableView?(self.tableView, commit: .delete, forRowAt: indexPath)\n                actionPerformed(true)\n            }))\n            alertView.addAction(UIAlertAction(title: \"Cancel\".localized, style: .cancel, handler: { _ in\n                actionPerformed(false)\n            }))\n            self.present(alertView, animated: true, completion: nil)\n        }\n\n        let configuration = UISwipeActionsConfiguration(actions: [action])\n        return configuration\n    }\n}\n\nextension MessageListViewController: UISearchControllerDelegate {\n    func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {\n        if self.navigationItem.searchController?.searchBar.isFirstResponder == true {\n            self.navigationItem.searchController?.searchBar.resignFirstResponder()\n        }\n    }\n\n    func willDismissSearchController(_ searchController: UISearchController) {\n        if !searchController.searchBar.isFirstResponder {\n            /*\n             searchBar 不在焦点时，点击搜索框右边的取消按钮时，不会触发 searchBar.rx.text 更改事件\n             searchBar.rx.text 将一直保留为最后的文本\n             但我们预期是要更新为 nil 的，因为再次点击searchBar，searchBar.text 显示的是 nil\n             可能对用户造成困惑，搜索框里没有输入任何keyword，但消息列表却被错误的keyword过滤了\n             \n             另外直接给 text 赋值，并不能触发 searchBar.rx.text，\n             需要手动发送一下actions\n             */\n            searchController.searchBar.searchTextField.text = nil\n            searchController.searchBar.searchTextField.sendActions(for: .editingDidEnd)\n        }\n    }\n    \n    func scrollViewDidScroll(_ scrollView: UIScrollView) {\n        let offset = scrollView.contentOffset.y + scrollView.adjustedContentInset.top\n        if offset <= -10 && canRefresh {\n            // 触发下拉刷新，并震动\n            canRefresh = false\n            UIImpactFeedbackGenerator(style: .light).impactOccurred()\n            refreshRelay.accept(())\n        }\n    }\n\n    func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {\n        let offset = scrollView.contentOffset.y + scrollView.adjustedContentInset.top\n        if offset >= 0 && !canRefresh {\n            canRefresh = true\n        }\n    }\n}\n"
  },
  {
    "path": "Controller/MessageListViewModel.swift",
    "content": "//\n//  MessageListViewModel.swift\n//  Bark\n//\n//  Created by huangfeng on 2020/11/21.\n//  Copyright © 2020 Fin. All rights reserved.\n//\n\nimport Foundation\nimport RealmSwift\nimport RxCocoa\nimport RxDataSources\nimport RxSwift\n\nenum MessageListType: Int, Codable {\n    // 列表\n    case list\n    // 分组\n    case group\n}\n\nenum MessageSourceType {\n    /// 全部数据源\n    case all\n    /// 只查看某一个分组\n    case group(String?)\n}\n\nclass MessageListViewModel: ViewModel, ViewModelType {\n    struct Input {\n        /// 刷新\n        var refresh: Driver<Void>\n        /// 加载更多\n        var loadMore: Driver<Void>\n        /// 删除\n        var itemDelete: Driver<MessageListCellItem>\n        /// 删除群组中某一条消息\n        var itemDeleteInGroup: Driver<MessageItemModel>\n        /// 批量删除\n        var delete: Driver<MessageDeleteTimeRange>\n        /// 切换群组和列表显示样式\n        var groupToggleTap: Driver<Void>\n        /// 搜索\n        var searchText: Observable<String?>\n        /// 重新刷新已加载的页的数据 （最多10页，超过则不刷新）\n        var reload: Driver<Void>\n    }\n    \n    struct Output {\n        /// 数据源\n        var messages: Driver<[MessageSection]>\n        /// 刷新控件状态\n        var refreshAction: Driver<MJRefreshAction>\n        /// 群组过滤\n        var type: Driver<MessageListType>\n        /// 标题\n        var title: Driver<String>\n        /// 群组切换按钮是否隐藏\n        var groupToggleButtonHidden: Driver<Bool>\n        /// 错误提示\n        var errorAlert: Driver<String>\n    }\n\n    private static let typeKey = \"me.fin.messageListType\"\n    /// 当前显示类型\n    private var type: MessageListType = {\n        if let t: MessageListType = Settings[MessageListViewModel.typeKey] {\n            return t\n        }\n        return .list\n    }() {\n        didSet {\n            Settings[MessageListViewModel.typeKey] = type\n        }\n    }\n    \n    /// 数据源\n    private var sourceType: MessageSourceType = .all\n    \n    /// 当前页数\n    private var page = 0\n    /// 每页数量\n    private let pageCount = 20\n    \n    /// 全部群组\n    private var groups: Results<Message>?\n    /// 全部数据（懒加载）\n    private var results: Results<Message>?\n    \n    private var errorAlert: PublishRelay<String> = PublishRelay()\n    \n    convenience init(sourceType: MessageSourceType) {\n        self.init()\n        self.sourceType = sourceType\n    }\n    \n    /// 获取筛选后的全部数据源 （懒加载）\n    private func getResults(filterGroups: [String?], searchText: String?) -> Results<Message>? {\n        do {\n            let realm = try Realm()\n            var results = realm.objects(Message.self)\n                .sorted(byKeyPath: \"createDate\", ascending: false)\n            if filterGroups.count > 0 {\n                results = results.filter(\"group in %@\", filterGroups)\n            }\n            if let text = searchText, text.count > 0 {\n                results = results.filter(\"title CONTAINS[c] %@ OR subtitle CONTAINS[c] %@ OR body CONTAINS[c] %@\", text, text, text)\n            }\n            return results\n        } catch {\n            // 延迟1秒后触发, 防止事件还没监听\n            DispatchQueue.main.asyncAfter(deadline: .now() + 1) {\n                self.errorAlert.accept(error.localizedDescription)\n            }\n        }\n        return nil\n    }\n    \n    /// 当前正在搜索的文字\n    private var searchText: String = \"\"\n    \n    /// 获取所有群组（懒加载）\n    private func getGroups() -> Results<Message>? {\n        if let realm = try? Realm() {\n            return realm.objects(Message.self)\n                .sorted(byKeyPath: \"createDate\", ascending: false)\n                .distinct(by: [\"group\"])\n        }\n        return nil\n    }\n\n    /// 获取 message 列表下一页数据\n    private func getListNextPage(page: Int, pageCount: Int) -> [MessageListCellItem] {\n        guard let result = results else {\n            return []\n        }\n        let startIndex = page * pageCount\n        let endIndex = min(startIndex + pageCount, result.count)\n        guard endIndex > startIndex else {\n            return []\n        }\n        var messages: [MessageListCellItem] = []\n        for i in startIndex..<endIndex {\n            // messages.append(result[i].freeze())\n            // 不用 freeze 是还没弄明白 freeze 冻结快照释放时机，先直接copy吧\n            // copy 是因为 message 可能在被删除后，还会被访问导致闪退\n            messages.append(.message(model: MessageItemModel(message: result[i])))\n        }\n        return messages\n    }\n\n    /// 获取 group 列表下一页数据\n    private func getGroupNextPage(page: Int, pageCount: Int) -> [MessageListCellItem] {\n        guard let groups, let results else {\n            return []\n        }\n        \n        let startIndex = page * pageCount\n        let endIndex = min(startIndex + pageCount, groups.count)\n        guard endIndex > startIndex else {\n            return []\n        }\n\n        var items: [MessageListCellItem] = []\n        \n        for i in startIndex..<endIndex {\n            let group = groups[i].group\n            let messageResult = getMessages(in: results, group: group)\n                \n            var messages: [MessageItemModel] = []\n            for i in 0..<min(messageResult.count, 5) {\n                messages.append(MessageItemModel(message: messageResult[i]))\n            }\n            if messages.count == 1 {\n                // 只有一条，就不要折叠\n                items.append(.message(model: messages[0]))\n            } else if messages.count > 0 {\n                // 多条消息时，折叠显示\n                items.append(.messageGroup(name: group ?? \"default\".localized, totalCount: messageResult.count, messages: messages))\n            }\n        }\n        return items\n    }\n\n    /// 使用 groupName 获取 messages\n    func getMessages(in results: Results<Message>, group: String?) -> Results<Message> {\n        if let group {\n            return results.filter(\"group == %@\", group)\n        } else {\n            return results.filter(\"group == nil\")\n        }\n    }\n    \n    private func getPage(page: Int, pageCount: Int) -> [MessageListCellItem] {\n        if case .group = self.sourceType {\n            // 查看指定分组时，只能按列表查看\n            return getListNextPage(page: page, pageCount: pageCount)\n        }\n        if type == .list || !searchText.isEmpty {\n            // 搜索时，也必须按列表查看\n            return getListNextPage(page: page, pageCount: pageCount)\n        }\n        return getGroupNextPage(page: page, pageCount: pageCount)\n    }\n    \n    private func getNextPage() -> [MessageListCellItem] {\n        defer {\n            page += 1\n        }\n        return getPage(page: self.page, pageCount: self.pageCount)\n    }\n    \n    func transform(input: Input) -> Output {\n        // 标题\n        let titleRelay = BehaviorRelay<String>(value: \"historyMessage\".localized)\n        // 数据源\n        let messagesRelay = BehaviorRelay<[MessageSection]>(value: [])\n        // 刷新操作\n        let refreshAction = BehaviorRelay<MJRefreshAction>(value: .none)\n        // 切换群组\n        let filterGroups: BehaviorRelay<[String?]> = { [weak self] in\n            guard let self = self else {\n                return BehaviorRelay<[String?]>(value: [])\n            }\n            if case .group(let name) = self.sourceType {\n                return BehaviorRelay<[String?]>(value: [name])\n            }\n            return BehaviorRelay<[String?]>(value: [])\n        }()\n        \n        // 切换分组时，更新分组名\n        filterGroups\n            .subscribe(onNext: { filterGroups in\n                if filterGroups.count <= 0 {\n                    titleRelay.accept(\"historyMessage\".localized)\n                } else {\n                    titleRelay.accept(filterGroups.map { $0 ?? \"default\".localized }.joined(separator: \" , \"))\n                }\n            }).disposed(by: rx.disposeBag)\n        \n        // 切换分组和更改搜索词时，更新数据源\n        Observable\n            .combineLatest(filterGroups, input.searchText)\n            .subscribe(onNext: { [weak self] groups, searchText in\n                self?.searchText = searchText ?? \"\"\n                self?.results = self?.getResults(filterGroups: groups, searchText: searchText)\n                if case .all = self?.sourceType {\n                    // 只有显示全部数据源时，才需要获取群组\n                    self?.groups = self?.getGroups()\n                }\n            }).disposed(by: rx.disposeBag)\n\n        // 群组筛选\n        let messageTypeChanged = input.groupToggleTap.compactMap { () -> MessageListType? in\n            self.type = self.type == .group ? .list : .group\n            return self.type\n        }\n\n        // 切换分组和下拉刷新时，重新刷新列表\n        Observable\n            .merge(\n                input.refresh.asObservable().map { () },\n                input.searchText.asObservable().map { _ in () },\n                messageTypeChanged.asObservable().map { _ in () }\n            )\n            .subscribe(onNext: { [weak self] in\n                guard let strongSelf = self else { return }\n                strongSelf.page = 0\n                let messages = strongSelf.getNextPage()\n                messagesRelay.accept(\n                    [MessageSection(header: \"model\", messages: messages)]\n                )\n                refreshAction.accept(.endRefresh)\n            }).disposed(by: rx.disposeBag)\n        \n        // 加载更多\n        // delay 是为了防止翻到 1+N 页时，切换分组操作（或其他）时会和 loadMore 同时触发，导致 Reentrancy anomaly，\n        // APP闪退报 “UITableView is trying to layout cells with a global row ...”。\n        input.loadMore.asObservable()\n            .delay(.milliseconds(10), scheduler: MainScheduler.instance)\n            .subscribe(onNext: { [weak self] in\n                guard let strongSelf = self else { return }\n                let items = strongSelf.getNextPage()\n                \n                refreshAction.accept(.endLoadmore)\n                if var section = messagesRelay.value.first {\n                    section.messages.append(contentsOf: items)\n                    messagesRelay.accept([section])\n                } else {\n                    messagesRelay.accept([MessageSection(header: \"model\", messages: items)])\n                }\n            }).disposed(by: rx.disposeBag)\n        \n        // 重新加载已加载的页的数据，最多10页\n        input.reload.drive(onNext: { [weak self] _ in\n            guard let self, self.page > 0, self.page <= 10 else { return }\n            // 刷新已加载的页的数据\n            let messages = self.getPage(page: 0, pageCount: self.page * self.pageCount)\n            messagesRelay.accept(\n                [MessageSection(header: \"model\", messages: messages)]\n            )\n        }).disposed(by: rx.disposeBag)\n        \n        // 删除message\n        input.itemDelete.drive(onNext: { [weak self] item in\n            guard let self else { return }\n            \n            guard var section = messagesRelay.value.first else {\n                return\n            }\n            \n            // 根据类型删除数据\n            switch item {\n            case .message(let model):\n                // 删除数据库对应消息\n                if let realm = try? Realm(),\n                   let message = realm.objects(Message.self).filter(\"id == %@\", model.id).first\n                {\n                    try? realm.write {\n                        realm.delete(message)\n                    }\n                }\n                // 删除 cell item\n                section.messages.removeAll { cellItem in\n                    if case .message(let m) = cellItem {\n                        return m.id == model.id\n                    }\n                    return false\n                }\n            case .messageGroup(let groupName, _, let messages):\n                // 删除数据库中对应分组\n                if let realm = try? Realm(), let first = messages.first {\n                    let messageResult: Results<Message>?\n                    if let group = first.group {\n                        messageResult = self.results?.filter(\"group == %@\", group)\n                    } else {\n                        messageResult = self.results?.filter(\"group == nil\")\n                    }\n                    if let messageResult {\n                        try? realm.write {\n                            realm.delete(messageResult)\n                        }\n                    }\n                }\n                // 删除 cell item\n                section.messages.removeAll { cellItem in\n                    if case .messageGroup(let name, _, _) = cellItem {\n                        return name == groupName\n                    }\n                    return false\n                }\n            }\n            \n            // 应用更改\n            messagesRelay.accept([section])\n            \n        }).disposed(by: rx.disposeBag)\n        \n        // 删除群组中某一条消息\n        input.itemDeleteInGroup.drive(onNext: { [weak self] model in\n            guard let self, let results else { return }\n            \n            guard var section = messagesRelay.value.first else {\n                return\n            }\n            \n            // 删除数据库里的 message\n            if let realm = try? Realm(),\n               let message = realm.objects(Message.self).filter(\"id == %@\", model.id).first\n            {\n                try? realm.write {\n                    realm.delete(message)\n                }\n            }\n            \n            if let index = section.messages.firstIndex(where: { item in\n                if case .messageGroup(_, _, let messages) = item {\n                    return messages.contains { item in\n                        return item.id == model.id\n                    }\n                }\n                return false\n            }) {\n                // 用最新的数据，重新生成 cellItem\n                if case .messageGroup(let name, _, var messages) = section.messages[index] {\n                    let messagesResult = self.getMessages(in: results, group: messages.first?.group)\n                    messages = messagesResult.prefix(5).map { MessageItemModel(message: $0) }\n                    if messages.count == 0 {\n                        section.messages.remove(at: index)\n                    } else {\n                        section.messages[index] = .messageGroup(name: name, totalCount: messagesResult.count, messages: messages)\n                    }\n                }\n            }\n            \n            messagesRelay.accept([section])\n            \n        }).disposed(by: rx.disposeBag)\n        \n        // 批量删除\n        input.delete.drive(onNext: { [weak self] range in\n            guard let self else { return }\n            \n            if let realm = try? Realm() {\n                guard let messages = self.getResults(filterGroups: filterGroups.value, searchText: nil)?.filter(\"createDate >= %@ and createDate <= %@ \", range.startDate, range.endDate) else {\n                    return\n                }\n                \n                try? realm.write {\n                    realm.delete(messages)\n                }\n            }\n            \n            self.page = 0\n            messagesRelay.accept([MessageSection(header: \"model\", messages: self.getNextPage())])\n            \n        }).disposed(by: rx.disposeBag)\n        \n        // 查看指定分组时，隐藏分组切换按钮\n        let groupToggleButtonHidden = {\n            if case .group = self.sourceType {\n                return true\n            }\n            return false\n        }()\n        \n        return Output(\n            messages: messagesRelay.asDriver(onErrorJustReturn: []),\n            refreshAction: refreshAction.asDriver(),\n            type: Driver.merge(messageTypeChanged.asDriver(), Driver.just(self.type)),\n            title: titleRelay.asDriver(),\n            groupToggleButtonHidden: Driver.just(groupToggleButtonHidden),\n            errorAlert: errorAlert.asDriver(onErrorDriveWith: .empty())\n        )\n    }\n}\n"
  },
  {
    "path": "Controller/MessageSettingsViewController.swift",
    "content": "//\n//  MessageSettingsViewController.swift\n//  Bark\n//\n//  Created by huangfeng on 2020/5/28.\n//  Copyright © 2020 Fin. All rights reserved.\n//\n\nimport Material\nimport RxCocoa\nimport RxDataSources\nimport RxSwift\nimport SVProgressHUD\nimport SwiftyStoreKit\nimport UIKit\nimport UniformTypeIdentifiers\n\nclass MessageSettingsViewController: BaseViewController<MessageSettingsViewModel>, UIDocumentPickerDelegate {\n    lazy var tableView: UITableView = {\n        let tableView = UITableView(frame: CGRect.zero, style: .insetGrouped)\n        tableView.separatorColor = BKColor.grey.lighten3\n        tableView.backgroundColor = BKColor.background.primary\n        tableView.register(LabelCell.self, forCellReuseIdentifier: \"\\(LabelCell.self)\")\n        tableView.register(ArchiveSettingCell.self, forCellReuseIdentifier: \"\\(ArchiveSettingCell.self)\")\n        tableView.register(DetailTextCell.self, forCellReuseIdentifier: \"\\(DetailTextCell.self)\")\n        tableView.register(MutableTextCell.self, forCellReuseIdentifier: \"\\(MutableTextCell.self)\")\n        tableView.register(SpacerCell.self, forCellReuseIdentifier: \"\\(SpacerCell.self)\")\n        tableView.register(DonateCell.self, forCellReuseIdentifier: \"\\(DonateCell.self)\")\n        \n        if #available(iOS 26.0, *) {\n            // iOS26 修改的圆角，将其改回\n            tableView.setValue(10, forKeyPath: \"sectionCornerRadius\")\n        }\n\n        tableView.estimatedSectionHeaderHeight = 10\n        tableView.sectionHeaderHeight = UITableView.automaticDimension\n        \n        let footer = MessageSettingFooter()\n        footer.openLinkHandler = { [weak self] link in\n            self?.openLink(link: link)\n        }\n        tableView.tableFooterView = footer\n        \n        tableView.delegate = self\n        return tableView\n    }()\n\n    private var headers: [String?] = []\n    private var footers: [String?] = []\n    \n    override func makeUI() {\n        self.title = \"settings\".localized\n        \n        self.view.addSubview(tableView)\n        tableView.snp.makeConstraints { make in\n            make.top.bottom.equalToSuperview()\n            make.left.right.equalTo(self.view.safeAreaLayoutGuide)\n        }\n    \n        // 捐赠内购没有任何逻辑，就不往 ViewModel 里放了，在这里处理一下得了\n        self.tableView.rx.modelSelected(MessageSettingItem.self).asObservable().compactMap { item in\n            if case .donate(_, let productId) = item {\n                return productId\n            }\n            return nil\n        }.subscribe(onNext: { [weak self] productId in\n            SVProgressHUD.show()\n            SwiftyStoreKit.purchaseProduct(productId) { result in\n                SVProgressHUD.dismiss()\n                if case .success = result {\n                    let alert = UIAlertController(title: \"successfulDonation\".localized, message: \"thankYouSupport\".localized, preferredStyle: .alert)\n                    alert.addAction(UIAlertAction(title: \"donateOK\".localized, style: .default, handler: nil))\n                    self?.present(alert, animated: true)\n                }\n            }\n        }).disposed(by: rx.disposeBag)\n    }\n    \n    ///  导入、导出操作枚举\n    enum BackupOrRestoreActionEnum {\n        case export, `import`(data: Data)\n    }\n    \n    /// UIDocumentPickerDelegate delegate\n    func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {\n        guard let url = urls.first else { return }\n        let canAccessingResource = url.startAccessingSecurityScopedResource()\n        guard canAccessingResource else { return }\n        \n        let fileCoordinator = NSFileCoordinator()\n        let err = NSErrorPointer(nilLiteral: ())\n        fileCoordinator.coordinate(readingItemAt: url, error: err) { url in\n            if let data = try? Data(contentsOf: url) {\n                self.backupOrRestoreActionRelay.accept(.import(data: data))\n            }\n        }\n        url.stopAccessingSecurityScopedResource()\n    }\n    \n    /// 导入、导出事件\n    let backupOrRestoreActionRelay = PublishRelay<BackupOrRestoreActionEnum>()\n    \n    /// 生成导入导出事件\n    func getBackupOrRestoreAction() -> (Driver<Void>, Driver<Data>) {\n        let backupOrRestoreAction = self.tableView.rx\n//            .modelSelected(MessageSettingItem.self)\n            .itemSelected\n            .filter { indexPath in\n                guard let viewModel: MessageSettingItem = try? self.tableView.rx.model(at: indexPath) else {\n                    return false\n                }\n                if case MessageSettingItem.backup = viewModel {\n                    return true\n                }\n                return false\n            }\n            .flatMapLatest { [weak self] indexPath in\n                guard let strongSelf = self else {\n                    return Observable<BackupOrRestoreActionEnum>.empty()\n                }\n            \n                let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)\n                alertController.addAction(UIAlertAction(title: \"export\".localized, style: .default, handler: { _ in\n                    strongSelf.backupOrRestoreActionRelay.accept(.export)\n                }))\n\n                alertController.addAction(UIAlertAction(title: \"import\".localized, style: .default, handler: { [weak self] _ in\n                    if #available(iOS 14.0, *) {\n                        let supportedType: [UTType] = [UTType.json]\n                        let pickerViewController = UIDocumentPickerViewController(forOpeningContentTypes: supportedType, asCopy: false)\n                        pickerViewController.delegate = self\n                        self?.present(pickerViewController, animated: true, completion: nil)\n                    }\n                }))\n\n                alertController.addAction(UIAlertAction(title: \"Cancel\".localized, style: .cancel, handler: nil))\n                if UIDevice.current.userInterfaceIdiom == .pad {\n                    if let cell = strongSelf.tableView.cellForRow(at: indexPath) {\n                        alertController.popoverPresentationController?.sourceView = strongSelf.tableView\n                        alertController.popoverPresentationController?.sourceRect = cell.frame\n                        alertController.modalPresentationStyle = .popover\n                    }\n                }\n                strongSelf.present(alertController, animated: true, completion: nil)\n\n                return strongSelf.backupOrRestoreActionRelay.asObservable()\n            }\n        \n        let backupAction = backupOrRestoreAction\n            .filter { action in\n                if case .export = action { return true } else { return false }\n            }\n            .map { _ in () }\n            .asDriver(onErrorDriveWith: .empty())\n        \n        let restoreAction = backupOrRestoreAction\n            .compactMap { action in\n                if case .import(let data) = action { return data } else { return nil }\n            }\n            .asDriver(onErrorDriveWith: .empty())\n        \n        return (backupAction, restoreAction)\n    }\n    \n    override func bindViewModel() {\n        let actions = getBackupOrRestoreAction()\n        let output = viewModel.transform(\n            input: MessageSettingsViewModel.Input(\n                itemSelected: self.tableView.rx.modelSelected(MessageSettingItem.self).asDriver(),\n                deviceToken: Client.shared.deviceToken.asDriver(),\n                backupAction: actions.0,\n                restoreAction: actions.1,\n                viewDidAppear: self.rx.methodInvoked(#selector(viewDidAppear(_:)))\n                    .map { _ in () },\n                archiveSettingRelay: ArchiveSettingRelay.shared.isArchiveRelay\n            )\n        )\n        \n        let dataSource = RxTableViewSectionedReloadDataSource<SectionModel<MessageSettingSection, MessageSettingItem>> { _, tableView, _, item -> UITableViewCell in\n            switch item {\n            case .label(let text):\n                if let cell = tableView.dequeueReusableCell(withIdentifier: \"\\(LabelCell.self)\") as? LabelCell {\n                    cell.textLabel?.text = text\n                    return cell\n                }\n            case .backup(let viewModel):\n                if let cell = tableView.dequeueReusableCell(withIdentifier: \"\\(MutableTextCell.self)\") as? MutableTextCell {\n                    cell.textLabel?.textColor = BKColor.blue.darken1\n                    cell.bindViewModel(model: viewModel)\n                    return cell\n                }\n            case .archiveSetting(let viewModel):\n                if let cell = tableView.dequeueReusableCell(withIdentifier: \"\\(ArchiveSettingCell.self)\") as? ArchiveSettingCell {\n                    cell.bindViewModel(model: viewModel)\n                    return cell\n                }\n            case .detail(let title, let text, let textColor, _):\n                if let cell = tableView.dequeueReusableCell(withIdentifier: \"\\(DetailTextCell.self)\") as? DetailTextCell {\n                    cell.textLabel?.text = title\n                    cell.detailTextLabel?.text = text\n                    cell.detailTextLabel?.textColor = textColor\n                    return cell\n                }\n            case .deviceToken(let viewModel):\n                if let cell = tableView.dequeueReusableCell(withIdentifier: \"\\(MutableTextCell.self)\") as? MutableTextCell {\n                    cell.bindViewModel(model: viewModel)\n                    return cell\n                }\n            case .spacer(let height, let color):\n                if let cell = tableView.dequeueReusableCell(withIdentifier: \"\\(SpacerCell.self)\") as? SpacerCell {\n                    cell.height = height\n                    cell.backgroundColor = color\n                    return cell\n                }\n            case .donate(let title, let productId):\n                if let cell = tableView.dequeueReusableCell(withIdentifier: \"\\(DonateCell.self)\") as? DonateCell {\n                    cell.title = title\n                    cell.productId = productId\n                    return cell\n                }\n            }\n            \n            return UITableViewCell()\n        }\n        \n        // 设置项的 header、footer\n        output.settings\n            .drive(onNext: { [weak self] settings in\n                self?.headers.removeAll()\n                self?.footers.removeAll()\n                for section in settings {\n                    self?.headers.append(section.model.header)\n                    self?.footers.append(section.model.footer)\n                }\n            })\n            .disposed(by: rx.disposeBag)\n        // 设置项数据源\n        output.settings\n            .drive(tableView.rx.items(dataSource: dataSource))\n            .disposed(by: rx.disposeBag)\n     \n        // 打开 URL 操作\n        output.openUrl.drive { [weak self] url in\n            self?.navigationController?.present(BarkSFSafariViewController(url: url), animated: true, completion: nil)\n        }.disposed(by: rx.disposeBag)\n        \n        // 复制 deviceToken 操作\n        output.copyDeviceToken.drive { [weak self] deviceToken in\n            UIPasteboard.general.string = deviceToken\n            self?.showSnackbar(text: \"Copy\".localized)\n        }.disposed(by: rx.disposeBag)\n        \n        // 导出数据\n        output.exportData.drive { [weak self] data in\n            \n            let fileManager = FileManager.default\n            let tempDirectoryURL = fileManager.temporaryDirectory\n            let fileName = \"bark_messages_\\(Date().formatString(format: \"yyyy_MM_dd_HH_mm_ss\")).json\"\n            let linkURL = tempDirectoryURL.appendingPathComponent(fileName)\n            \n            do {\n                // 清空temp文件夹\n                try fileManager\n                    .contentsOfDirectory(at: tempDirectoryURL, includingPropertiesForKeys: nil, options: .skipsSubdirectoryDescendants)\n                    .forEach { file in\n                        try? fileManager.removeItem(atPath: file.path)\n                    }\n                // 写入临时文件\n                try data.write(to: linkURL)\n            } catch {\n                // Hope nothing happens\n            }\n            \n            let activityController = UIActivityViewController(activityItems: [linkURL], applicationActivities: nil)\n            if UIDevice.current.userInterfaceIdiom == .pad {\n                activityController.popoverPresentationController?.sourceView = self?.view\n                activityController.popoverPresentationController?.sourceRect = self?.view.frame ?? .zero\n            }\n            self?.navigationController?.present(activityController, animated: true, completion: nil)\n            \n        }.disposed(by: rx.disposeBag)\n    }\n    \n    func openLink(link: String) {\n        switch link {\n        case \"privacyPolicy\":\n            self.navigationController?.present(BarkSFSafariViewController(\n                url: URL(string: \"https://api.day.app/privacy\")!\n            ), animated: true, completion: nil)\n        case \"userAgreement\":\n            self.navigationController?.present(BarkSFSafariViewController(\n                url: URL(string: \"https://www.apple.com/legal/internet-services/itunes/dev/stdeula\")!\n            ), animated: true, completion: nil)\n        case \"restoreSubscription\":\n            SwiftyStoreKit.restorePurchases { [weak self] _ in\n                self?.showSnackbar(text: \"done\".localized)\n            }\n        default: break\n        }\n    }\n}\n\nextension MessageSettingsViewController: UITableViewDelegate {\n    func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {\n        guard self.headers.count > section, let header = self.headers[section] else { return UIView() }\n        \n        let headerView = SettingSectionHeader()\n        headerView.titleLabel.text = header\n        return headerView\n    }\n\n    func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {\n        guard self.footers.count > section, let footer = self.footers[section] else { return UIView() }\n        let footerView = SettingSectionFooter()\n        footerView.titleLabel.text = footer\n        return footerView\n    }\n    \n    /// FUCK iOS, insetGrouped 和 tableView.sectionFooterHeight = UITableView.automaticDimension 一起用有BUG，因参与计算的宽度口径不一致导致高度可能计算不准确\n    /// 只能自己计算了\n    func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {\n        guard self.footers.count > section, let footer = self.footers[section] else { return 10 }\n        // 16 是 tableView 左右的间距， 12 是 uilabel 左右的间距\n        let size = CGSize(width: tableView.frame.width - 16 * 2 - 12 * 2, height: .greatestFiniteMagnitude)\n        let rect = (footer as NSString).boundingRect(\n            with: size,\n            options: [.usesLineFragmentOrigin],\n            attributes: [.font: UIFont.preferredFont(ofSize: 12)],\n            context: nil\n        )\n        // 8: top offset, 6：bottom offset\n        return rect.height + 8 + 6\n    }\n}\n"
  },
  {
    "path": "Controller/MessageSettingsViewModel.swift",
    "content": "//\n//  MessageSettingsViewModel.swift\n//  Bark\n//\n//  Created by huangfeng on 2020/11/20.\n//  Copyright © 2020 Fin. All rights reserved.\n//\n\nimport Foundation\nimport Material\nimport RealmSwift\nimport RxCocoa\nimport RxDataSources\nimport RxSwift\nimport SwiftyJSON\n\nclass MessageSettingsViewModel: ViewModel, ViewModelType {\n    struct Input {\n        var itemSelected: Driver<MessageSettingItem>\n        var deviceToken: Driver<String?>\n        var backupAction: Driver<Void>\n        var restoreAction: Driver<Data>\n        var viewDidAppear: Observable<Void>\n        var archiveSettingRelay: BehaviorRelay<Bool>\n    }\n\n    struct Output {\n        var settings: Driver<[SectionModel<MessageSettingSection, MessageSettingItem>]>\n        var openUrl: Driver<URL>\n        var copyDeviceToken: Driver<String>\n        var exportData: Driver<Data>\n    }\n\n    func transform(input: Input) -> Output {\n        let restoreSuccess = input\n            .restoreAction\n            .compactMap { data -> Void? in\n                guard let json = try? JSON(data: data), let arr = json.array else {\n                    return nil\n                }\n                guard let realm = try? Realm() else {\n                    return nil\n                }\n                try? realm.write {\n                    for message in arr {\n                        guard let messageObject = Message(json: message) else {\n                            continue\n                        }\n                        realm.add(messageObject, update: .modified)\n                    }\n                }\n                return ()\n            }.asObservable().share()\n\n        let settings: [SectionModel<MessageSettingSection, MessageSettingItem>] = {\n            var settings = [SectionModel<MessageSettingSection, MessageSettingItem>]()\n            \n            // 历史消息\n            var messageSettings = [MessageSettingItem]()\n            messageSettings.append(.backup(viewModel: MutableTextCellViewModel(\n                title: \"\\(\"export\".localized)/\\(\"import\".localized)\",\n                text: Observable.merge([restoreSuccess, input.viewDidAppear])\n                    .map { _ in\n                        if let realm = try? Realm() {\n                            return realm.objects(Message.self)\n                                .count\n                        }\n                        return 0\n                    }\n                    .map { count in\n                        \"\\(count) \\(\"items\".localized)\"\n                    }\n                    .asDriver(onErrorDriveWith: .empty())\n            )\n            ))\n            \n            messageSettings.append(.archiveSetting(viewModel: ArchiveSettingCellViewModel(on: input.archiveSettingRelay)))\n            \n            settings.append(\n                SectionModel(\n                    model: MessageSettingSection(header: \"historyMessage\".localized, footer: \"archiveNote\".localized),\n                    items: messageSettings\n                )\n            )\n            \n            // 信息\n            var infosettings = [MessageSettingItem]()\n            infosettings.append(.deviceToken(\n                viewModel: MutableTextCellViewModel(\n                    title: \"Device Token\",\n                    text: input\n                        .deviceToken\n                        .map {\n                            deviceToken in\n                            if let deviceToken = deviceToken {\n                                return \"\\(deviceToken.prefix(2))****\\(deviceToken.suffix(4))\"\n                            }\n                            return \"unknown\".localized\n                        }\n                )\n            ))\n\n            if let infoDict = Bundle.main.infoDictionary,\n               let runId = infoDict[\"GitHub Run Id\"] as? String\n            {\n                infosettings.append(.detail(\n                    title: \"Github Run Id\",\n                    text: \"\\(runId)\",\n                    textColor: BKColor.grey.darken2,\n                    url: URL(string: \"https://github.com/Finb/Bark/actions/runs/\\(runId)\")\n                ))\n            }\n            settings.append(\n                SectionModel(\n                    model: MessageSettingSection(header: \"info\".localized, footer: \"buildDesc\".localized),\n                    items: infosettings\n                )\n            )\n            \n            // 其他\n            var otherSettings = [MessageSettingItem]()\n            otherSettings.append(.detail(\n                title: \"faq\".localized,\n                text: nil,\n                textColor: nil,\n                url: URL(string: \"faqUrl\".localized)\n            ))\n\n            otherSettings.append(.detail(\n                title: \"documentation\".localized,\n                text: nil,\n                textColor: nil,\n                url: URL(string: \"docUrl\".localized)\n            ))\n            otherSettings.append(.detail(\n                title: \"sourceCode\".localized,\n                text: nil,\n                textColor: nil,\n                url: URL(string: \"https://github.com/Finb/Bark\")\n            ))\n            \n            settings.append(\n                SectionModel(\n                    model: MessageSettingSection(header: \"other\".localized),\n                    items: otherSettings\n                )\n            )\n            \n            // 捐赠\n            var donateSettings = [MessageSettingItem]()\n            donateSettings.append(.donate(title: \"oneTimeDonation\".localized, productId: \"bark.oneTimeDonation.18\"))\n            donateSettings.append(.donate(title: \"continuousSupport\".localized, productId: \"bark.continuousSupport.18\"))\n            settings.append(\n                SectionModel(\n                    model: MessageSettingSection(\n                        header: \"donate\".localized,\n                        footer: nil\n                    ),\n                    items: donateSettings\n                )\n            )\n            \n            return settings\n        }()\n\n        let openUrl = input.itemSelected.compactMap { item -> URL? in\n            if case MessageSettingItem.detail(_, _, _, let url) = item {\n                return url\n            }\n            return nil\n        }\n\n        let deviceTokenValue: BehaviorRelay<String?> = BehaviorRelay(value: nil)\n        input.deviceToken.drive(deviceTokenValue)\n            .disposed(by: rx.disposeBag)\n        let copyDeviceToken = input.itemSelected.compactMap { item -> String? in\n            if case MessageSettingItem.deviceToken = item {\n                return deviceTokenValue.value\n            }\n            return nil\n        }\n\n        // 导出数据\n        let exportSuccess = input.backupAction\n            .asObservable()\n            .subscribe(on: ConcurrentDispatchQueueScheduler(qos: .userInitiated))\n            .compactMap { _ in\n                if let realm = try? Realm() {\n                    let messages = realm.objects(Message.self)\n                        .sorted(byKeyPath: \"createDate\", ascending: false)\n\n                    var arr = [[String: AnyObject]]()\n                    for message in messages {\n                        arr.append(message.toDictionary())\n                    }\n                    return try? JSON(arr).rawData(options: JSONSerialization.WritingOptions.prettyPrinted)\n                }\n                return nil\n            }\n\n        return Output(\n            settings: Driver<[SectionModel<MessageSettingSection, MessageSettingItem>]>\n                .just(settings),\n            openUrl: openUrl,\n            copyDeviceToken: copyDeviceToken,\n            exportData: exportSuccess.asDriver(onErrorDriveWith: .empty())\n        )\n    }\n}\n\nenum MessageSettingItem {\n    // 普通标题标签\n    case label(text: String)\n    // 默认保存\n    case archiveSetting(viewModel: ArchiveSettingCellViewModel)\n    // 带 详细按钮的 文本cell\n    case detail(title: String?, text: String?, textColor: UIColor?, url: URL?)\n    // 备份还原按钮\n    case backup(viewModel: MutableTextCellViewModel)\n    // deviceToken\n    case deviceToken(viewModel: MutableTextCellViewModel)\n    // 分隔线\n    case spacer(height: CGFloat, color: UIColor?)\n    // 捐赠\n    case donate(title: String, productId: String)\n}\n\nstruct MessageSettingSection {\n    var header: String?\n    var footer: String?\n}\n"
  },
  {
    "path": "Controller/NewServerViewController.swift",
    "content": "//\n//  NewServerViewController.swift\n//  Bark\n//\n//  Created by huangfeng on 2018/6/25.\n//  Copyright © 2018 Fin. All rights reserved.\n//\n\nimport Material\nimport RxCocoa\nimport RxSwift\nimport SafariServices\nimport SnapKit\nimport UIKit\n\nclass NewServerViewController: BaseViewController<NewServerViewModel> {\n    let scanButton: BKButton = {\n        let button = BKButton()\n        button.setImage(UIImage(named: \"baseline_qr_code_scanner_black_24pt\"), for: .normal)\n        button.frame = CGRect(x: 0, y: 0, width: 24, height: 24)\n        button.hitTestSlop = UIEdgeInsets(top: -10, left: -10, bottom: -10, right: -10)\n        button.tintColor = BKColor.grey.darken3\n        button.isAccessibilityElement = false\n        return button\n    }()\n    \n    lazy var addressTextField: TextField = {\n        let textField = TextField()\n        textField.keyboardType = .URL\n        textField.placeholder = \"ServerAddress\".localized\n        textField.detail = \"ServerExample\".localized\n        textField.transition([.scale(0.85), .opacity(0)])\n        textField.detailLabel.transition([.scale(0.85), .opacity(0)])\n        textField.textColor = BKColor.grey.darken4\n        textField.placeholderNormalColor = BKColor.grey.base\n        textField.detailLabel.textColor = BKColor.grey.base\n        \n        textField.rightView?.grid.views = [scanButton]\n        textField.rightViewMode = .whileEditing\n        return textField\n    }()\n    \n    let noticeLabel: UILabel = {\n        let label = UILabel()\n        label.text = \"DeploymentDocuments\".localized\n        label.textColor = BKColor.blue.base\n        label.font = UIFont.preferredFont(ofSize: 12)\n        label.adjustsFontForContentSizeCategory = true\n        label.transition([.scale(0.85), .opacity(0), .translate(x: 50)])\n        label.isUserInteractionEnabled = true\n        label.addGestureRecognizer(UITapGestureRecognizer())\n        return label\n    }()\n    \n    lazy var doneButton: BKButton = {\n        let doneButton = BKButton()\n        doneButton.setImage(Icon.check, for: .normal)\n        doneButton.frame = CGRect(x: 0, y: 0, width: 40, height: 40)\n        navigationItem.setRightBarButtonItem(item: UIBarButtonItem(customView: doneButton))\n        doneButton.tintColor = BKColor.grey.darken4\n        return doneButton\n    }()\n    \n    override func makeUI() {\n        self.navigationItem.largeTitleDisplayMode = .never\n        navigationItem.title = \"AddServer\".localized\n\n        self.view.addSubview(addressTextField)\n        self.view.addSubview(noticeLabel)\n        addressTextField.snp.makeConstraints { make in\n            make.top.equalTo(kNavigationHeight + 40)\n            make.left.right.equalTo(self.view.safeAreaLayoutGuide).inset(16)\n        }\n        noticeLabel.snp.makeConstraints { make in\n            make.top.equalTo(self.addressTextField.snp.bottom).offset(40)\n            make.left.equalTo(self.addressTextField)\n        }\n    }\n\n    override func bindViewModel() {\n        // 点击提醒按钮事件\n        let noticeTap = noticeLabel.gestureRecognizers!.first!.rx\n            .event\n            .map { _ in\n                ()\n            }\n            .asDriver(onErrorJustReturn: ())\n        \n        // 点击完成按钮事件\n        let done = doneButton.rx.tap\n            .map { [weak self] in\n                self?.addressTextField.text ?? \"\"\n            }\n            .asDriver(onErrorDriveWith: .empty())\n        \n        // 页面显示事件\n        let viewDidAppear = rx\n            .methodInvoked(#selector(viewDidAppear(_:)))\n            .map { _ in () }\n            .asDriver(onErrorDriveWith: .empty())\n        \n        // 扫描二维码事件\n        let scannerDidScan = self.scanButton.rx.tap.flatMapLatest { [weak self] _ -> Observable<String> in\n            let controller = QRScannerViewController()\n            self?.navigationController?.present(controller, animated: true, completion: nil)\n            return controller.scannerDidSuccess\n        }.asDriver(onErrorDriveWith: .empty())\n        \n        let output = viewModel.transform(\n            input: NewServerViewModel.Input(\n                noticeClick: noticeTap,\n                done: done,\n                viewDidAppear: viewDidAppear,\n                didScan: scannerDidScan\n            ))\n        \n        // 键盘显示与隐藏\n        output.showKeyboard.drive(onNext: { [weak self] show in\n            if show {\n                _ = self?.addressTextField.becomeFirstResponder()\n            } else {\n                self?.addressTextField.resignFirstResponder()\n            }\n        }).disposed(by: rx.disposeBag)\n        \n        // 点击教程\n        output.notice.drive(onNext: { [weak self] url in\n            self?.navigationController?.present(BarkSFSafariViewController(url: url), animated: true, completion: nil)\n        }).disposed(by: rx.disposeBag)\n        \n        // URL文本框文本\n        output.urlText\n            .drive(self.addressTextField.rx.text)\n            .disposed(by: rx.disposeBag)\n        \n        // 退出页面\n        output.pop.drive(onNext: { [weak self] _ in\n            self?.navigationController?.popViewController(animated: true)\n        }).disposed(by: rx.disposeBag)\n        \n        // 弹出提示文本\n        output.showSnackbar.drive(onNext: { [weak self] text in\n            self?.showSnackbar(text: text)\n        }).disposed(by: rx.disposeBag)\n    }\n}\n"
  },
  {
    "path": "Controller/NewServerViewModel.swift",
    "content": "//\n//  NewServerViewModel.swift\n//  Bark\n//\n//  Created by huangfeng on 2020/11/18.\n//  Copyright © 2020 Fin. All rights reserved.\n//\n\nimport Foundation\nimport Moya\nimport RxCocoa\nimport RxSwift\nimport SwiftyJSON\n\nclass NewServerViewModel: ViewModel, ViewModelType {\n    struct Input {\n        var noticeClick: Driver<Void>\n        var done: Driver<String>\n        var viewDidAppear: Driver<Void>\n        var didScan: Driver<String>\n    }\n    \n    struct Output {\n        var showKeyboard: Driver<Bool>\n        var notice: Driver<URL>\n        var urlText: Driver<String>\n        var showSnackbar: Driver<String>\n        var pop: Driver<String>\n    }\n    \n    private var url: String = \"\"\n    \n    let pop = PublishRelay<String>()\n    \n    func transform(input: Input) -> Output {\n        let showKeyboard = PublishRelay<Bool>()\n        let urlText = PublishRelay<String>()\n        let showSnackbar = PublishRelay<String>()\n\n        let notice = input.noticeClick\n            .map { URL(string: \"deployUrl\".localized)! }\n            .asDriver()\n        \n        input.viewDidAppear\n            .map { \"https://\" }\n            .asObservable()\n            .take(1)\n            .subscribe(onNext: { text in\n                showKeyboard.accept(true)\n                urlText.accept(text)\n            }).disposed(by: rx.disposeBag)\n        \n        input.didScan.compactMap { text in\n            URL(string: text)\n        }.drive(onNext: { url in\n            urlText.accept(url.absoluteString)\n        }).disposed(by: rx.disposeBag)\n        \n        input.done\n            .asObservable()\n            .flatMapLatest { [weak self] url -> Observable<Result<JSON, ApiError>> in\n                showKeyboard.accept(false)\n                if let _ = URL(string: url) {\n                    guard let strongSelf = self else { return .empty() }\n                    strongSelf.url = url\n                    return BarkApi.provider\n                        .request(.ping(baseURL: url))\n                        .filterResponseError()\n                } else {\n                    showSnackbar.accept(\"InvalidURL\".localized)\n                    return .empty()\n                }\n            }\n            .subscribe(onNext: { [weak self] response in\n                guard let strongSelf = self else { return }\n                switch response {\n                case .success:\n                    let server = Server(address: strongSelf.url, key: \"\")\n                    ServerManager.shared.addServer(server: server)\n                    ServerManager.shared.setCurrentServer(serverId: server.id)\n                    ServerManager.shared.syncAllServers()\n                    \n                    strongSelf.pop.accept(URL(string: strongSelf.url)?.host ?? \"\")\n                    showSnackbar.accept(\"AddedSuccessfully\".localized)\n                case .failure(let error):\n                    showSnackbar.accept(\"\\(\"InvalidServer\".localized)\\(error.rawString())\")\n                }\n            }).disposed(by: rx.disposeBag)\n\n        return Output(\n            showKeyboard: showKeyboard.asDriver(onErrorDriveWith: .empty()),\n            notice: notice,\n            urlText: urlText.asDriver(onErrorDriveWith: .empty()),\n            showSnackbar: showSnackbar.asDriver(onErrorDriveWith: .empty()),\n            pop: pop.asDriver(onErrorDriveWith: .empty())\n        )\n    }\n}\n"
  },
  {
    "path": "Controller/QRScannerViewController.swift",
    "content": "//\n//  QRScannerViewController.swift\n//  Bark\n//\n//  Created by huangfeng on 2022/3/10.\n//  Copyright © 2022 Fin. All rights reserved.\n//\n\nimport MercariQRScanner\nimport RxCocoa\nimport RxSwift\nimport UIKit\n\nclass QRScannerViewController: UIViewController {\n    var scannerDidSuccess: Observable<String> {\n        return self.rx.methodInvoked(#selector(didSeccess(code:))).map { a in\n            try castOrThrow(String.self, a[0])\n        }\n    }\n\n    let closeButton: UIButton = {\n        let closeButton = UIButton(type: .custom)\n        closeButton.setImage(UIImage(named: \"baseline_close_white_48pt\"), for: .normal)\n        closeButton.tintColor = UIColor.white\n        closeButton.backgroundColor = UIColor(white: 0, alpha: 0.2)\n        closeButton.layer.cornerRadius = 40\n        closeButton.clipsToBounds = true\n        return closeButton\n    }()\n\n    override func viewDidLoad() {\n        super.viewDidLoad()\n        self.view.backgroundColor = UIColor.black\n\n        let qrScannerView = QRScannerView(frame: view.bounds)\n        qrScannerView.configure(delegate: self)\n        view.addSubview(qrScannerView)\n\n        view.addSubview(closeButton)\n        closeButton.snp.makeConstraints { make in\n            make.bottom.equalToSuperview().offset(-120)\n            make.centerX.equalToSuperview()\n            make.width.height.equalTo(80)\n        }\n        closeButton.rx.tap.subscribe { [weak self] in\n            self?.dismiss(animated: true, completion: nil)\n        } onError: { _ in }.disposed(by: rx.disposeBag)\n\n        qrScannerView.startRunning()\n    }\n}\n\nextension QRScannerViewController: QRScannerViewDelegate {\n    func qrScannerView(_ qrScannerView: QRScannerView, didFailure error: QRScannerError) {\n        self.showSnackbar(text: error.rawString())\n    }\n\n    func qrScannerView(_ qrScannerView: QRScannerView, didSuccess code: String) {\n        self.didSeccess(code: code)\n        self.dismiss(animated: true, completion: nil)\n    }\n\n    @objc private func didSeccess(code: String) {}\n}\n"
  },
  {
    "path": "Controller/SectionViewController-iPad.swift",
    "content": "//\n//  SectionTableViewController-iPad.swift\n//  Bark\n//\n//  Created by sidguan on 2024/6/23.\n//  Copyright © 2024 Fin. All rights reserved.\n//\n\nimport UIKit\n\nimport NSObject_Rx\nimport RxCocoa\nimport RxDataSources\nimport RxSwift\n\nclass SectionViewController_iPad: BaseViewController<SectionViewModel>, UITableViewDelegate {\n    let tableView: UITableView = {\n        let tableView = UITableView(frame: .zero, style: .grouped)\n        tableView.register(UITableViewCell.self, forCellReuseIdentifier: \"\\(UITableViewCell.self)\")\n        tableView.backgroundColor = BKColor.background.primary\n        return tableView\n    }()\n    \n    let homeController = BarkNavigationController(rootViewController: HomeViewController(viewModel: HomeViewModel()))\n    let messageListController = BarkNavigationController(rootViewController: MessageListViewController(viewModel: MessageListViewModel()))\n    let settingsController = BarkNavigationController(rootViewController: MessageSettingsViewController(viewModel: MessageSettingsViewModel()))\n    \n    var viewControllers: [UIViewController] {\n        [\n            homeController,\n            messageListController,\n            settingsController\n        ]\n    }\n    \n    override func viewDidLoad() {\n        super.viewDidLoad()\n        self.title = \"Bark\"\n        navigationItem.largeTitleDisplayMode = .automatic\n    }\n    \n    override func makeUI() {\n        self.view.addSubview(tableView)\n        tableView.delegate = self\n        tableView.snp.makeConstraints { make in\n            make.edges.equalToSuperview()\n        }\n        \n        tableView.rx\n            .itemSelected\n            .flatMapLatest { indexPath -> Observable<IndexPath> in\n                return Observable.just(indexPath)\n            }\n            .subscribe { [weak self] indexPath in\n                guard let self, indexPath.row < self.viewControllers.count else {\n                    return\n                }\n                self.splitViewController?.showDetailViewController(self.viewControllers[indexPath.row], sender: self)\n                Settings[.selectedViewControllerIndex] = indexPath.row\n            }.disposed(by: rx.disposeBag)\n    }\n    \n    override func bindViewModel() {\n        let output = viewModel.transform(input: SectionViewModel.Input())\n        let dataSource = RxTableViewSectionedReloadDataSource<SectionModel<String, SectionItem>> {\n            _, tableView, _, item -> UITableViewCell in\n            guard let cell = tableView.dequeueReusableCell(withIdentifier: \"\\(UITableViewCell.self)\") else {\n                return UITableViewCell()\n            }\n            cell.selectionStyle = .gray\n            cell.imageView?.image = item.image\n            cell.imageView?.tintColor = BKColor.grey.darken4\n            cell.textLabel?.text = item.title\n            return cell\n        }\n        output.items\n            .bind(to: tableView.rx.items(dataSource: dataSource))\n            .disposed(by: rx.disposeBag)\n    }\n\n    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {\n        return 55\n    }\n\n    func tableView(_ tableView: UITableView, canFocusRowAt indexPath: IndexPath) -> Bool {\n        // 去掉额外的蓝色 selectionStyle\n        return false\n    }\n}\n"
  },
  {
    "path": "Controller/SectionViewModel-iPad.swift",
    "content": "//\n//  SectionViewModel-iPad.swift\n//  Bark\n//\n//  Created by sidguan on 2024/7/1.\n//  Copyright © 2024 Fin. All rights reserved.\n//\n\nimport Foundation\nimport Material\nimport RxCocoa\nimport RxDataSources\nimport RxSwift\n\nstruct SectionItem {\n    let image: UIImage?\n    let title: String\n}\n\nclass SectionViewModel: ViewModel, ViewModelType {\n    struct Input {\n//        var sectionSelected: Driver<SectionItem>\n    }\n    \n    struct Output {\n        var items: Observable<[SectionModel<String, SectionItem>]>\n//        var selectedItem: Observable<SectionItem?>\n    }\n    \n    func initSectionItems() -> Observable<[SectionModel<String, SectionItem>]> {\n        let sectionItems = [\n            SectionItem(image: UIImage(named: \"baseline_gite_black_24pt\"), title: \"service\".localized),\n            SectionItem(image: Icon.history, title: \"historyMessage\".localized),\n            SectionItem(image: UIImage(named: \"baseline_manage_accounts_black_24pt\"), title: \"settings\".localized)\n        ]\n        let section = [SectionModel(model: \"\", items: sectionItems)]\n        return Observable.just(section)\n    }\n    \n    func transform(input: Input) -> Output {\n        let sectionItems = initSectionItems()\n        return Output(\n            items: sectionItems\n        )\n    }\n}\n"
  },
  {
    "path": "Controller/ServerListViewController.swift",
    "content": "//\n//  ServerListViewController.swift\n//  Bark\n//\n//  Created by huangfeng on 2022/3/25.\n//  Copyright © 2022 Fin. All rights reserved.\n//\n\nimport Material\nimport RxCocoa\nimport RxDataSources\nimport RxSwift\nimport UIKit\n\nenum ServerActionType {\n    case select\n    case copy\n    case reset(key: String?)\n    case delete\n    case setName(name: String?)\n}\n\nfunc == (lhs: ServerActionType, rhs: ServerActionType) -> Bool {\n    switch (lhs, rhs) {\n    case (.copy, .copy),\n         (.delete, .delete),\n         (.select, .select):\n        return true\n    case (.reset(let a), .reset(let b)):\n        return a == b\n    default:\n        return false\n    }\n}\n\nclass ServerListViewController: BaseViewController<ServerListViewModel> {\n    let closeButton: BKButton = {\n        let closeButton = BKButton()\n        closeButton.setImage(UIImage(named: \"baseline_keyboard_arrow_down_black_24pt\")?.withRenderingMode(.alwaysTemplate), for: .normal)\n        closeButton.frame = CGRect(x: 0, y: 0, width: 40, height: 40)\n        closeButton.hitTestSlop = UIEdgeInsets(top: -10, left: -10, bottom: -10, right: -10)\n        closeButton.tintColor = BKColor.grey.darken4\n        closeButton.accessibilityLabel = \"close\".localized\n        return closeButton\n    }()\n\n    let tableView: UITableView = {\n        let tableView = UITableView()\n        tableView.separatorStyle = .none\n        tableView.backgroundColor = BKColor.background.primary\n        tableView.register(ServerListTableViewCell.self, forCellReuseIdentifier: \"\\(ServerListTableViewCell.self)\")\n        tableView.contentInset = UIEdgeInsets(top: 20, left: 0, bottom: 0, right: 0)\n        return tableView\n    }()\n\n    override func makeUI() {\n        self.title = \"serverList\".localized\n\n        navigationItem.setRightBarButtonItem(item: UIBarButtonItem(customView: closeButton))\n\n        self.view.addSubview(tableView)\n        tableView.snp.makeConstraints { make in\n            make.edges.equalToSuperview()\n        }\n        \n        closeButton.rx.tap.subscribe { [weak self] in\n            self?.dismiss(animated: true, completion: nil)\n        } onError: { _ in\n            \n        }.disposed(by: rx.disposeBag)\n    }\n\n    override func bindViewModel() {\n        let action = getServerAction()\n\n        // 选择 server\n        let selectServer = action.filter { $0.1 == ServerActionType.select }\n            .map { $0.0 }.asDriver(onErrorDriveWith: .empty())\n        \n        // 复制 server\n        let copyServer = action.filter { $0.1 == ServerActionType.copy }\n            .map { $0.0 }.asDriver(onErrorDriveWith: .empty())\n\n        // 删除 server\n        let deleteServer = action.filter { $0.1 == ServerActionType.delete }\n            .map { $0.0 }.asDriver(onErrorDriveWith: .empty())\n\n        // 重置 server key\n        let resetServer = action.compactMap { r -> (Server, String?)? in\n            if case ServerActionType.reset(let key) = r.1 {\n                return (r.0, key)\n            }\n            return nil\n        }.asDriver(onErrorDriveWith: .empty())\n        \n        // 设置服务器名称\n        let setServerName = action.compactMap { r -> (Server, String?)? in\n            if case ServerActionType.setName(let name) = r.1 {\n                return (r.0, name)\n            }\n            return nil\n        }.asDriver(onErrorDriveWith: .empty())\n\n        let output = viewModel.transform(input: ServerListViewModel.Input(\n            selectServer: selectServer,\n            copyServer: copyServer,\n            deleteServer: deleteServer,\n            resetServer: resetServer,\n            setServerName: setServerName\n        ))\n\n        let dataSource = RxTableViewSectionedReloadDataSource<SectionModel<String, ServerListTableViewCellViewModel>> { _, tableView, _, item -> UITableViewCell in\n            if let cell = tableView.dequeueReusableCell(withIdentifier: \"\\(ServerListTableViewCell.self)\") as? ServerListTableViewCell {\n                cell.bindViewModel(model: item)\n                return cell\n            }\n            return UITableViewCell()\n        }\n\n        // TableView数据源\n        output.servers\n            .drive(self.tableView.rx.items(dataSource: dataSource))\n            .disposed(by: rx.disposeBag)\n\n        // 复制文本\n        output.copy\n            .drive(onNext: { [weak self] text in\n                UIPasteboard.general.string = text\n                self?.showSnackbar(text: \"Copy\".localized)\n            })\n            .disposed(by: rx.disposeBag)\n\n        // 弹出提示\n        output.showSnackbar\n            .drive(onNext: { [weak self] text in\n                self?.showSnackbar(text: text)\n            })\n            .disposed(by: rx.disposeBag)\n    }\n\n    func getServerAction() -> Driver<(Server, ServerActionType)> {\n        return tableView.rx\n            .itemSelected\n            .flatMapLatest { indexPath in\n                let relay = PublishRelay<(Server, ServerActionType)>()\n                guard let viewModel: ServerListTableViewCellViewModel = try? self.tableView.rx.model(at: indexPath) else {\n                    return relay\n                }\n\n                let alertController = UIAlertController(title: nil, message: \"\\(URL(string: viewModel.server.address)?.host ?? \"\")\", preferredStyle: .actionSheet)\n                alertController.addAction(UIAlertAction(title: \"copyAddressAndKey\".localized, style: .default, handler: { _ in\n                    relay.accept((viewModel.server, .copy))\n                }))\n\n                alertController.addAction(UIAlertAction(title: \"resetKey\".localized, style: .default, handler: { _ in\n                    let alertController = UIAlertController(title: \"resetKey\".localized, message: \"resetKeyDesc\".localized, preferredStyle: .alert)\n                    alertController.addTextField { textField in\n                        textField.placeholder = \"resetKeyPlaceholder\".localized\n                    }\n                    alertController.addAction(UIAlertAction(title: \"confirm\".localized, style: .default, handler: { _ in\n                        relay.accept((viewModel.server, .reset(key: alertController.textFields?.first?.text)))\n                    }))\n                    alertController.addAction(UIAlertAction(title: \"Cancel\".localized, style: .cancel, handler: nil))\n                    self.navigationController?.present(alertController, animated: true, completion: nil)\n                }))\n\n                alertController.addAction(UIAlertAction(title: \"setAsDefaultServer\".localized, style: .default, handler: { _ in\n                    relay.accept((viewModel.server, .select))\n                }))\n\n                alertController.addAction(UIAlertAction(title: \"setServerName\".localized, style: .default, handler: { _ in\n                    let alertController = UIAlertController(title: \"setServerName\".localized, message: nil, preferredStyle: .alert)\n                    alertController.addTextField { textField in\n                        textField.text = viewModel.server.name\n                    }\n                    alertController.addAction(UIAlertAction(title: \"confirm\".localized, style: .default, handler: { _ in\n                        relay.accept((viewModel.server, .setName(name: alertController.textFields?.first?.text)))\n                    }))\n                    alertController.addAction(UIAlertAction(title: \"Cancel\".localized, style: .cancel, handler: nil))\n                    self.navigationController?.present(alertController, animated: true, completion: nil)\n                }))\n\n                alertController.addAction(UIAlertAction(title: \"deleteServer\".localized, style: .destructive, handler: { _ in\n\n                    let alertController = UIAlertController(title: nil, message: \"confirmDeleteServer\".localized, preferredStyle: .alert)\n                    alertController.addAction(UIAlertAction(title: \"confirm\".localized, style: .destructive, handler: { _ in\n                        relay.accept((viewModel.server, .delete))\n                    }))\n                    alertController.addAction(UIAlertAction(title: \"Cancel\".localized, style: .cancel, handler: nil))\n                    self.navigationController?.present(alertController, animated: true, completion: nil)\n\n                }))\n\n                alertController.addAction(UIAlertAction(title: \"Cancel\".localized, style: .cancel, handler: nil))\n                \n                if UIDevice.current.userInterfaceIdiom == .pad {\n                    if let cell = self.tableView.cellForRow(at: indexPath) {\n                        alertController.popoverPresentationController?.sourceView = self.tableView\n                        alertController.popoverPresentationController?.sourceRect = cell.frame\n                        alertController.modalPresentationStyle = .popover\n                    }\n                }\n                self.navigationController?.present(alertController, animated: true, completion: nil)\n\n                return relay\n            }.asDriver(onErrorDriveWith: .empty())\n    }\n}\n"
  },
  {
    "path": "Controller/ServerListViewModel.swift",
    "content": "//\n//  ServerListViewModel.swift\n//  Bark\n//\n//  Created by huangfeng on 2022/3/25.\n//  Copyright © 2022 Fin. All rights reserved.\n//\n\nimport Differentiator\nimport Foundation\nimport Moya\nimport RxCocoa\nimport RxSwift\nimport SwiftyJSON\n\nclass ServerListViewModel: ViewModel, ViewModelType {\n    struct Input {\n        let selectServer: Driver<Server>\n        let copyServer: Driver<Server>\n        let deleteServer: Driver<Server>\n        let resetServer: Driver<(Server, String?)>\n        let setServerName: Driver<(Server, String?)>\n    }\n\n    struct Output {\n        let servers: Driver<[SectionModel<String, ServerListTableViewCellViewModel>]>\n        let showSnackbar: Driver<String>\n        let copy: Driver<String>\n    }\n\n    let currentServerChanged = PublishRelay<Server>()\n\n    func transform(input: Input) -> Output {\n        // 弹出提示消息\n        let showSnackbar = PublishRelay<String>()\n\n        // 复制 Server\n        let copy = input.copyServer.map { server -> String in\n            \"\\(server.address)/\\(server.key)/\"\n        }\n        \n        // 设置服务器名称\n        input.setServerName.drive(onNext: { server, name in\n            ServerManager.shared.setServerName(server: server, name: name)\n        }).disposed(by: rx.disposeBag)\n\n        // 删除检查，需要至少保留一个服务器\n        let deleteCheck = input.deleteServer.map { server -> Server? in\n            if ServerManager.shared.servers.count > 1 {\n                return server\n            }\n            return nil\n        }.asObservable().share()\n\n        // 删除检查错误提示\n        deleteCheck.filter { $0 == nil }\n            .map { _ in \"deleteFailed\".localized }\n            .bind(to: showSnackbar)\n            .disposed(by: rx.disposeBag)\n\n        // 对即将删除的服务器发送错误的 deviceToken，防止服务器依然保留旧的推送链接。\n        let delete = deleteCheck.compactMap { $0 }\n            .flatMapLatest { server -> Observable<Result<JSON, ApiError>>in\n                if server.key.count > 0 {\n                    return BarkApi.provider\n                        .request(.register(address: server.address, key: server.key, devicetoken: \"deleted\"))\n                        .filterResponseError()\n                }\n                return Observable.just(Result<JSON, ApiError>.success(JSON()))\n                    .delay(.milliseconds(300), scheduler: MainScheduler.instance)\n            }\n\n        // 服务器远程注销后，再本地删除\n        // withLatestFrom 将在 delete 产生新的事件时,\n        // 取 input.deleteServer 最后一个事件的元素。（这里是对应的 server ）\n        let serverDeleted = delete.withLatestFrom(input.deleteServer)\n            .map { server in\n                ServerManager.shared.removeServer(server: server)\n            }.share()\n\n        // 弹出删除提示\n        serverDeleted.map { \"deletedSuccessfully\".localized }\n            .bind(to: showSnackbar)\n            .disposed(by: rx.disposeBag)\n\n        // 重置服务器之前，先检查 DeviceToken\n        let resetServer = input.resetServer\n            .map { ($0.0, $0.1, Client.shared.deviceToken.value) }\n            .asObservable().share()\n\n        // 重置检查错误提示\n        resetServer.filter { ($0.2?.count ?? 0) <= 0 }\n            .map { _ in \"resetFailed2\".localized }\n            .bind(to: showSnackbar)\n            .disposed(by: rx.disposeBag)\n\n        // 对重置的旧 key 发送错误的 deviceToken, 使其失效。\n        resetServer.filter { ($0.2?.count ?? 0) > 0 && $0.0.key.count > 0 }.flatMapLatest {\n            BarkApi.provider\n                .request(.register(address: $0.0.address, key: $0.0.key, devicetoken: \"deleted\"))\n                .filterResponseError()\n        }\n        .subscribe()\n        .disposed(by: rx.disposeBag)\n\n        // 发送重置请求\n        let serverReseted = resetServer.filter { ($0.2?.count ?? 0) > 0 }\n            .flatMapLatest { r -> Observable<Result<JSON, ApiError>> in\n                let server = r.0\n                let newKey = r.1\n                let deviceToken = r.2!\n                return BarkApi.provider\n                    .request(.register(address: server.address, key: newKey, devicetoken: deviceToken))\n                    .filterResponseError()\n            }\n            .map { result -> String? in\n                switch result {\n                case .success(let json):\n                    return json[\"data\", \"key\"].rawString()\n                case .failure:\n                    return nil\n                }\n            }.share()\n\n        // 重置成功后，更新本地服务器列表\n        let serverResetSuccess = serverReseted.compactMap { $0 }.withLatestFrom(input.resetServer) { newKey, r in\n            let server = r.0\n            server.key = newKey\n            ServerManager.shared.updateServerKey(server: server)\n        }.share()\n\n        // 重置失败提示\n        serverReseted.filter { $0 == nil }\n            .map { _ in \"resetFailed\".localized }\n            .bind(to: showSnackbar)\n            .disposed(by: rx.disposeBag)\n\n        // 服务器列表\n        let servers = Observable\n            .merge(\n                Observable.just(()),\n                serverDeleted,\n                serverResetSuccess,\n                input.setServerName.map { _ in () }.asObservable()\n            )\n            .map {\n                [SectionModel(\n                    model: \"servers\",\n                    items: ServerManager.shared.servers.map { ServerListTableViewCellViewModel(server: $0) }\n                )]\n            }.asDriver(onErrorDriveWith: .empty())\n\n        // 选择首页预览服务器\n        let serverSelected = input.selectServer.asObservable().map { server in\n            ServerManager.shared.setCurrentServer(serverId: server.id)\n            showSnackbar.accept(\"setSuccessfully\".localized)\n            return ()\n        }\n        \n        // 当前服务器有改动\n        let serverChanged = Observable.merge(serverSelected, serverDeleted, serverResetSuccess)\n            .share()\n\n        serverChanged.map {\n            ServerManager.shared.currentServer\n        }\n        .bind(to: self.currentServerChanged)\n        .disposed(by: rx.disposeBag)\n\n        // 服务器改变时，同步 Client.shared.state。\n        serverChanged.map {\n            ServerManager.shared.currentServer.state\n        }\n        .bind(to: Client.shared.state)\n        .disposed(by: rx.disposeBag)\n\n        return Output(\n            servers: servers,\n            showSnackbar: showSnackbar.asDriver(onErrorDriveWith: .empty()),\n            copy: copy\n        )\n    }\n}\n"
  },
  {
    "path": "Controller/SoundsViewController.swift",
    "content": "//\n//  SoundsViewController.swift\n//  Bark\n//\n//  Created by huangfeng on 2020/9/14.\n//  Copyright © 2020 Fin. All rights reserved.\n//\n\nimport AVKit\nimport Material\nimport UIKit\n\nimport NSObject_Rx\nimport RxCocoa\nimport RxDataSources\nimport RxSwift\n\nclass SoundsViewController: BaseViewController<SoundsViewModel> {\n    let tableView: UITableView = {\n        let tableView = UITableView(frame: CGRect.zero, style: .insetGrouped)\n        tableView.backgroundColor = BKColor.background.primary\n        tableView.register(SoundCell.self, forCellReuseIdentifier: \"\\(SoundCell.self)\")\n        tableView.register(AddSoundCell.self, forCellReuseIdentifier: \"\\(AddSoundCell.self)\")\n        return tableView\n    }()\n    \n    // 上传铃声文件事件序列\n    let importSoundActionRelay = PublishRelay<URL>()\n    // 当前正在播放的音频资源ID\n    var currentSoundID: SystemSoundID = 0\n    // 当前正在播放的音频文件ULRL\n    var playingAudio: CFURL?\n\n    override func makeUI() {\n        self.title = \"notificationSound\".localized\n\n        self.view.addSubview(self.tableView)\n        self.tableView.delegate = self\n        self.tableView.snp.makeConstraints { make in\n            make.edges.equalToSuperview()\n        }\n    }\n\n    override func bindViewModel() {\n        let output = viewModel.transform(\n            input: SoundsViewModel.Input(\n                soundSelected: self.tableView.rx.modelSelected(SoundItem.self).asDriver(),\n                importSound: self.importSoundActionRelay.asDriver(onErrorDriveWith: .empty()),\n                soundDeleted: self.tableView.rx.modelDeleted(SoundItem.self).asDriver()\n            )\n        )\n\n        let dataSource = RxTableViewSectionedReloadDataSource<SectionModel<String, SoundItem>> { _, tableView, _, item -> UITableViewCell in\n            switch item {\n            case .sound(let model):\n                guard let cell = tableView.dequeueReusableCell(withIdentifier: \"\\(SoundCell.self)\") as? SoundCell else {\n                    return UITableViewCell()\n                }\n                cell.bindViewModel(model: model)\n                return cell\n            case .addSound:\n                guard let cell = tableView.dequeueReusableCell(withIdentifier: \"\\(AddSoundCell.self)\") else {\n                    return UITableViewCell()\n                }\n                return cell\n            }\n        } titleForHeaderInSection: { dataSource, section in\n            return dataSource[section].model\n        } canEditRowAtIndexPath: { dataSource, indexPath in\n            guard indexPath.section == 0 else {\n                return false\n            }\n            guard case SoundItem.sound = dataSource[indexPath.section].items[indexPath.row] else {\n                return false\n            }\n            return true\n        }\n\n        output.audios\n            .bind(to: tableView.rx.items(dataSource: dataSource))\n            .disposed(by: rx.disposeBag)\n\n        output.copyNameAction.drive(onNext: { [unowned self] name in\n            UIPasteboard.general.string = name.trimmingCharacters(in: .whitespacesAndNewlines)\n            self.navigationController?.showSnackbar(text: \"Copy\".localized)\n        }).disposed(by: rx.disposeBag)\n\n        output.playAction.drive(onNext: { url in\n            /// 先结束正在播放的音频\n            AudioServicesDisposeSystemSoundID(self.currentSoundID)\n            /// 如果重复点击了当前音频，结束播放\n            if self.playingAudio == url {\n                self.playingAudio = nil\n                self.currentSoundID = 0\n                return\n            }\n            self.playingAudio = url\n            AudioServicesCreateSystemSoundID(url, &self.currentSoundID)\n            AudioServicesPlaySystemSoundWithCompletion(self.currentSoundID) {\n                /// 判断是否是当前播放的音频，防止逻辑错误\n                if self.playingAudio == url {\n                    AudioServicesDisposeSystemSoundID(self.currentSoundID)\n                    self.playingAudio = nil\n                    self.currentSoundID = 0\n                }\n            }\n        }).disposed(by: rx.disposeBag)\n        \n        output.pickerFile.drive(onNext: { [unowned self] _ in\n            self.pickerSoundFile()\n        }).disposed(by: rx.disposeBag)\n    }\n}\n\nextension SoundsViewController: UITableViewDelegate {\n    func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {\n        return 40\n    }\n\n    func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {\n        guard section == 0 else {\n            return 0\n        }\n        return \"uploadSoundNoticeFullText\".localized.count <= 30 ? 50 : 60\n    }\n    \n    func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {\n        let sectionTitle = tableView.dataSource?.tableView?(tableView, titleForHeaderInSection: section) ?? \"\"\n        \n        let view = UIView()\n        \n        let label = UILabel()\n        label.text = sectionTitle.localized\n        label.fontSize = 14\n        label.textColor = BKColor.grey.darken3\n        view.addSubview(label)\n        label.snp.makeConstraints { make in\n            make.left.equalTo(12)\n            make.centerY.equalToSuperview()\n        }\n        \n        return view\n    }\n\n    func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {\n        guard section == 0 else {\n            return nil\n        }\n        let view = UIView()\n\n        let fullText = \"uploadSoundNoticeFullText\".localized\n        let highlightText = \"uploadSoundNoticeHighlightText\".localized\n        let attrStr = NSMutableAttributedString(\n            string: fullText,\n            attributes: [\n                NSAttributedString.Key.foregroundColor: BKColor.grey.darken3,\n                NSAttributedString.Key.font: UIFont.preferredFont(ofSize: 14)\n            ]\n        )\n        attrStr.setAttributes([\n            NSAttributedString.Key.foregroundColor: BKColor.lightBlue.darken3,\n            NSAttributedString.Key.font: UIFont.preferredFont(ofSize: 14)\n        ], range: (fullText as NSString).range(of: highlightText))\n        \n        let label = UILabel()\n        label.attributedText = attrStr\n        label.numberOfLines = 0\n        view.addSubview(label)\n        label.snp.makeConstraints { make in\n            make.left.equalTo(12)\n            make.right.equalTo(-12)\n            make.top.equalTo(12)\n        }\n        \n        label.isUserInteractionEnabled = true\n        label.addGestureRecognizer(UITapGestureRecognizer())\n        label.gestureRecognizers?.first?.rx.event.subscribe(onNext: { _ in\n            UIApplication.shared.open(URL(string: \"https://convertio.co/mp3-caf/\")!)\n        }).disposed(by: label.rx.disposeBag)\n        \n        return view\n    }\n}\n\nextension SoundsViewController: UIDocumentPickerDelegate {\n    /// 选择 caf 文件\n    func pickerSoundFile() {\n        if #available(iOS 14.0, *) {\n            /// 直接修改文件扩展为 .audio iOS 支持的音频格式\n            let documentPicker = UIDocumentPickerViewController(forOpeningContentTypes: [.audio])\n            documentPicker.delegate = self\n            documentPicker.allowsMultipleSelection = false\n            documentPicker.modalPresentationStyle = .pageSheet\n            self.present(documentPicker, animated: true, completion: nil)\n        } else {\n            self.showSnackbar(text: \"Requires iOS 14\")\n        }\n    }\n    \n    // 文件选择完成回调\n    func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {\n        guard let url = urls.first else { return }\n        let canAccessingResource = url.startAccessingSecurityScopedResource()\n        guard canAccessingResource else { return }\n        \n        let fileCoordinator = NSFileCoordinator()\n        let err = NSErrorPointer(nilLiteral: ())\n        fileCoordinator.coordinate(readingItemAt: url, error: err) { url in\n            // 检查文件扩展名是否为 caf\n            if url.pathExtension.lowercased() == \"caf\" {\n                // 如果是 caf 文件，直接导入\n                self.importSoundActionRelay.accept(url)\n            } else {\n                // 如果不是 caf 文件，先转换为 caf 格式\n                convertAudioToCAF(inputURL: url) { url in\n                    if let url {\n                        // 转换成功后导入\n                        self.importSoundActionRelay.accept(url)\n                    }\n                }\n            }\n        }\n        url.stopAccessingSecurityScopedResource()\n    }\n    \n    /// 将音频文件转换为 CAF 格式\n    /// - Parameters:\n    ///   - inputURL: 输入音频文件的 URL\n    ///   - completion: 转换完成后的回调，返回转换后的 CAF 文件 URL，如果转换失败则返回 nil\n    func convertAudioToCAF(inputURL: URL, completion: @escaping (URL?) -> Void) {\n        let fileName = inputURL.deletingPathExtension().lastPathComponent\n        let outputURL = FileManager.default.temporaryDirectory.appendingPathComponent(\"\\(fileName).caf\")\n\n        do {\n            if FileManager.default.fileExists(atPath: outputURL.path) {\n                try FileManager.default.removeItem(at: outputURL)\n            }\n\n            let asset = AVAsset(url: inputURL)\n            guard let exportSession = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetPassthrough) else {\n                completion(nil)\n                return\n            }\n\n            let maxDuration = CMTime(seconds: 29.9, preferredTimescale: 600)\n            if asset.duration > maxDuration {\n                exportSession.timeRange = CMTimeRange(start: .zero, duration: maxDuration)\n            }\n\n            exportSession.outputFileType = .caf\n            exportSession.outputURL = outputURL\n\n            exportSession.exportAsynchronously {\n                DispatchQueue.main.async {\n                    completion(exportSession.status == .completed ? outputURL : nil)\n                }\n            }\n\n        } catch {\n            completion(nil)\n        }\n    }\n}\n"
  },
  {
    "path": "Controller/SoundsViewModel.swift",
    "content": "//\n//  SoundsViewModel.swift\n//  Bark\n//\n//  Created by huangfeng on 2020/11/17.\n//  Copyright © 2020 Fin. All rights reserved.\n//\n\nimport AVKit\nimport Foundation\nimport RxCocoa\nimport RxDataSources\nimport RxSwift\n\nenum SoundItem {\n    case sound(model: SoundCellViewModel)\n    case addSound\n}\n\nclass SoundsViewModel: ViewModel, ViewModelType {\n    /// 依赖\n    struct Dependencies {\n        /// 用于保存铃声文件\n        let soundFileStorage: SoundFileStorageProtocol\n    }\n\n    private let dependencies: Dependencies\n    init(dependencies: Dependencies = Dependencies(soundFileStorage: SoundFileStorage())) {\n        self.dependencies = dependencies\n    }\n\n    struct Input {\n        /// 铃声列表点击\n        var soundSelected: Driver<SoundItem>\n        /// 铃声导入\n        var importSound: Driver<URL>\n        /// 删除铃声\n        var soundDeleted: Driver<SoundItem>\n    }\n\n    struct Output {\n        /// 铃声数据源\n        var audios: Observable<[SectionModel<String, SoundItem>]>\n        /// 复制铃声名称\n        var copyNameAction: Driver<String>\n        /// 播放铃声\n        var playAction: Driver<CFURL>\n        /// 打开文件选择器选择铃声文件\n        var pickerFile: Driver<Void>\n    }\n\n    /// 将铃声 URL 转换成 SoundItem\n    func getSounds(urls: [URL]) -> [SoundItem] {\n        let urls = urls.sorted { u1, u2 -> Bool in\n            u1.lastPathComponent.localizedStandardCompare(u2.lastPathComponent) == ComparisonResult.orderedAscending\n        }\n        return urls\n            .map { AVURLAsset(url: $0) }\n            .map { SoundCellViewModel(model: $0) }\n            .map { SoundItem.sound(model: $0) }\n    }\n\n    /// 返回指定文件夹，指定后缀的文件列表数组\n    func getFilesInDirectory(directory: String, suffix: String) -> [URL] {\n        let fileManager = FileManager.default\n        do {\n            let files = try fileManager.contentsOfDirectory(atPath: directory)\n            return files.compactMap { file -> URL? in\n                if file.hasSuffix(suffix), !file.hasPrefix(kBarkSoundPrefix) {\n                    // 不要包含 kBarkSoundPrefix 开头的，这些是为了 call=1 合成的 30s 长铃声,不算用户上传的\n                    return URL(fileURLWithPath: directory).appendingPathComponent(file)\n                }\n                return nil\n            }\n        } catch {\n            return []\n        }\n    }\n\n    func transform(input: Input) -> Output {\n        // 保存文件\n        input\n            .importSound\n            .drive { [unowned self] url in\n                self.dependencies.soundFileStorage.saveSound(url: url)\n            }\n            .disposed(by: rx.disposeBag)\n        \n        // 删除铃声\n        input.soundDeleted.drive(onNext: { item in\n            guard case SoundItem.sound(let model) = item else {\n                return\n            }\n            self.dependencies.soundFileStorage.deleteSound(name: model.model.url.lastPathComponent)\n        }).disposed(by: rx.disposeBag)\n        \n        // 铃声列表有更新\n        let soundsListUpdated = Observable.merge(\n            // 刚进页面\n            Observable.just(()),\n            // 上传了新铃声\n            input.importSound.map { _ in () }.asObservable(),\n            // 删除了铃声\n            input.soundDeleted.map { _ in () }.asObservable()\n        ).share(replay: 1)\n        \n        // 所有铃声列表，包含自定义铃声和默认铃声\n        let sounds: Observable<([SoundItem], [SoundItem])> = soundsListUpdated.map { [weak self] _ in\n            guard let self else { return ([], []) }\n            \n            let defaultSounds = self.getSounds(\n                urls: Bundle.main.urls(forResourcesWithExtension: \"caf\", subdirectory: nil) ?? []\n            )\n\n            let customSounds: [SoundItem] = {\n                guard let soundsDirectoryUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: \"group.bark\")?.appendingPathComponent(\"Library/Sounds\").path else {\n                    return [.addSound]\n                }\n                return self.getSounds(\n                    urls: self.getFilesInDirectory(directory: soundsDirectoryUrl, suffix: \"caf\")\n                ) + [.addSound]\n            }()\n            return (customSounds, defaultSounds)\n        }.share(replay: 1)\n        \n        // 用于 RxDataSource 的数据源\n        let dataSource = sounds.map { sounds in\n            return [\n                SectionModel(model: \"customSounds\", items: sounds.0),\n                SectionModel(model: \"defaultSounds\", items: sounds.1)\n            ]\n        }\n        \n        // 铃声列表点击复制按钮事件\n        let copyAction = sounds.flatMapLatest { sounds in\n            let observables = (sounds.0 + sounds.1).compactMap { item in\n                if case SoundItem.sound(let model) = item {\n                    return model\n                }\n                return nil\n            }.map { model in\n                return model.copyNameAction.asObservable()\n            }\n            return Observable.merge(observables)\n        }.asDriver(onErrorDriveWith: .empty())\n\n        return Output(\n            audios: dataSource,\n            copyNameAction: copyAction,\n            playAction: input.soundSelected\n                .compactMap { item in\n                    if case SoundItem.sound(let model) = item {\n                        return model\n                    }\n                    return nil\n                }\n                .map { $0.model.url as CFURL },\n            pickerFile: input.soundSelected\n                .compactMap { item in\n                    if case SoundItem.addSound = item {\n                        return ()\n                    }\n                    return nil\n                }\n        )\n    }\n}\n\n/// 保存铃声文件协议\nprotocol SoundFileStorageProtocol {\n    func saveSound(url: URL)\n    func deleteSound(name: String)\n}\n\n/// 用于将铃声文件保存在  /Library/Sounds 文件夹中\nclass SoundFileStorage: SoundFileStorageProtocol {\n    let fileManager: FileManager\n    init() {\n        fileManager = FileManager()\n    }\n\n    /// 将指定文件保存在 Library/Sound，如果存在则覆盖\n    func saveSound(url: URL) {\n        // 保存到Sounds文件夹\n        guard let soundsDirectoryUrl = getSoundsDirectory() else {\n            return\n        }\n        let soundUrl = soundsDirectoryUrl.appendingPathComponent(url.lastPathComponent)\n        try? fileManager.copyItem(at: url, to: soundUrl)\n    }\n\n    func deleteSound(name: String) {\n        guard let soundsDirectoryUrl = getSoundsDirectory() else {\n            return\n        }\n        let soundUrl = soundsDirectoryUrl.appendingPathComponent(name)\n        let callSoundUrl = soundsDirectoryUrl.appendingPathComponent(\"\\(kBarkSoundPrefix).\\(name)\")\n        // 删除 sounds 目录铃声文件\n        try? fileManager.removeItem(at: soundUrl)\n        // 删除 call=1 生成的铃声文件\n        try? fileManager.removeItem(at: callSoundUrl)\n    }\n\n    /// 获取 Library 目录下的 Sounds 文件夹\n    /// 如果不存在就创建\n    private func getSoundsDirectory() -> URL? {\n        guard let soundsDirectoryUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: \"group.bark\")?.appendingPathComponent(\"Library/Sounds\") else {\n            return nil\n        }\n        if !fileManager.fileExists(atPath: soundsDirectoryUrl.path) {\n            try? fileManager.createDirectory(atPath: soundsDirectoryUrl.path, withIntermediateDirectories: true, attributes: nil)\n        }\n        return URL(fileURLWithPath: soundsDirectoryUrl.path)\n    }\n}\n"
  },
  {
    "path": "Gemfile",
    "content": "source \"https://rubygems.org\"\n\ngem \"cocoapods\"\ngem \"fastlane\"\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2018 Feng\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "Model/Algorithm.swift",
    "content": "//\n//  Algorithm.swift\n//  Bark\n//\n//  Created by huangfeng on 2023/2/23.\n//  Copyright © 2023 Fin. All rights reserved.\n//\n\nimport CryptoSwift\nimport Foundation\n\nenum Algorithm: String {\n    case aes128 = \"AES128\"\n    case aes192 = \"AES192\"\n    case aes256 = \"AES256\"\n\n    var modes: [String] {\n        switch self {\n        case .aes128, .aes192, .aes256:\n            return [\"CBC\", \"ECB\", \"GCM\"]\n        }\n    }\n\n    var keyLength: Int {\n        switch self {\n        case .aes128:\n            return 16\n        case .aes192:\n            return 24\n        case .aes256:\n            return 32\n        }\n    }\n}\n\nstruct CryptoSettingFields: Codable {\n    let algorithm: String\n    let mode: String\n    let padding: String\n    let key: String?\n    var iv: String?\n}\n\nstruct AESCryptoModel {\n    let key: String\n    let mode: BlockMode\n    let padding: Padding\n    let aes: AES\n    init(cryptoFields: CryptoSettingFields) throws {\n        guard let algorithm = Algorithm(rawValue: cryptoFields.algorithm) else {\n            throw \"Invalid algorithm\"\n        }\n        guard let key = cryptoFields.key else {\n            throw \"Key is missing\"\n        }\n\n        guard algorithm.keyLength == key.count else {\n            throw String(format: \"enterKey\".localized, algorithm.keyLength)\n        }\n\n        var iv = \"\"\n        if [\"CBC\", \"GCM\"].contains(cryptoFields.mode) {\n            let expectIVLength = [\n                \"CBC\": 16,\n                \"GCM\": 12\n            ][cryptoFields.mode] ?? 0\n\n            if let ivField = cryptoFields.iv, ivField.count == expectIVLength {\n                iv = ivField\n            } else {\n                throw String(format: \"enterIv\".localized, expectIVLength)\n            }\n        }\n\n        let mode: BlockMode\n        switch cryptoFields.mode {\n        case \"CBC\":\n            mode = CBC(iv: iv.bytes)\n        case \"ECB\":\n            mode = ECB()\n        case \"GCM\":\n            mode = GCM(iv: iv.bytes, mode: .combined)\n        default:\n            throw \"Invalid Mode\"\n        }\n\n        let padding: Padding\n        switch cryptoFields.padding {\n        case \"noPadding\":\n            padding = .noPadding\n        case \"pkcs7\":\n            padding = .pkcs7\n        default:\n            throw \"Invalid Padding\"\n        }\n\n        self.key = key\n        self.mode = mode\n        self.padding = padding\n        self.aes = try AES(key: key.bytes, blockMode: self.mode, padding: self.padding)\n    }\n\n    func encrypt(text: String) throws -> String {\n        return try aes.encrypt(Array(text.utf8)).toBase64()\n    }\n\n    func decrypt(ciphertext: String) throws -> String {\n        return try String(data: Data(aes.decrypt(Array(base64: ciphertext))), encoding: .utf8) ?? \"\"\n    }\n}\n"
  },
  {
    "path": "Model/Message.swift",
    "content": "//\n//  Message.swift\n//  Bark\n//\n//  Created by huangfeng on 2020/5/25.\n//  Copyright © 2020 Fin. All rights reserved.\n//\n\nimport RealmSwift\nimport SwiftyJSON\nimport UIKit\n\nclass Message: Object {\n    enum BodyType: String {\n        case plainText\n        case markdown\n    }\n    @Persisted(primaryKey: true) var id = UUID().uuidString\n    @Persisted var title: String?\n    @Persisted var subtitle: String?\n    @Persisted var body: String?\n    @Persisted var bodyType: String?\n    @Persisted var url: String?\n    @Persisted var image: String?\n    @Persisted(indexed: true) var group: String?\n    @Persisted(indexed: true) var createDate: Date?\n\n    var type: BodyType {\n        get {\n            guard let bodyType = bodyType else {\n                return .plainText\n            }\n            return BodyType(rawValue: bodyType) ?? .plainText\n        }\n    }\n\n    /// 从 JSON 初始化\n    convenience init?(json: JSON) {\n        self.init()\n        guard let id = json[\"id\"].string else {\n            return nil\n        }\n        guard let createDate = json[\"createDate\"].int64 else {\n            return nil\n        }\n        self.id = id\n        self.title = json[\"title\"].string\n        self.subtitle = json[\"subtitle\"].string\n        self.body = json[\"body\"].string\n        self.bodyType = json[\"bodyType\"].string\n        self.url = json[\"url\"].string\n        self.image = json[\"image\"].string\n        self.group = json[\"group\"].string\n        self.createDate = Date(timeIntervalSince1970: TimeInterval(createDate))\n    }\n    \n    convenience init(dict: [String: Any]) {\n        self.init()\n        if let id = dict[\"id\"] as? String {\n            self.id = id\n        }\n        if let title = dict[\"title\"] as? String {\n            self.title = title\n        }\n        if let subtitle = dict[\"subtitle\"] as? String {\n            self.subtitle = subtitle\n        }\n        if let body = dict[\"body\"] as? String {\n            self.body = body\n        }\n        if let bodyType = dict[\"bodyType\"] as? String {\n            self.bodyType = bodyType\n        }\n        if let url = dict[\"url\"] as? String {\n            self.url = url\n        }\n        if let image = dict[\"image\"] as? String {\n            self.image = image\n        }\n        if let group = dict[\"group\"] as? String {\n            self.group = group\n        }\n        if let createDateInterval = dict[\"createDate\"] as? TimeInterval {\n            self.createDate = Date(timeIntervalSince1970: createDateInterval)\n        }\n    }\n}\n"
  },
  {
    "path": "Model/MessageDeleteTimeRange.swift",
    "content": "//\n//  MessageDeleteTimeRange.swift\n//  Bark\n//\n//  Created by huangfeng on 1/7/25.\n//  Copyright © 2025 Fin. All rights reserved.\n//\n\nimport Foundation\n\nenum MessageDeleteTimeRange {\n    /// 最近一小时\n    case lastHour\n    /// 今天\n    case today\n    /// 今天和昨天\n    case todayAndYesterday\n    /// 最近一个月\n    case lastMonth\n    /// 全部时间\n    case allTime\n    \n    /// 一小时之前\n    case beforeOneHour\n    /// 一天之前\n    case beforeToday\n    /// 昨天之前\n    case beforeYesterday\n    /// 一月之前\n    case beforeOneMonth\n     \n    var string: String {\n        switch self {\n        case .lastHour:\n            return \"lastHour\".localized\n        case .today:\n            return \"today\".localized\n        case .todayAndYesterday:\n            return \"todayAndYesterday\".localized\n        case .lastMonth:\n            return \"lastMonth\".localized\n        case .allTime:\n            return \"allTime\".localized\n        case .beforeOneHour:\n            return \"beforeAnHour\".localized\n        case .beforeToday:\n            return \"beforeToday\".localized\n        case .beforeYesterday:\n            return \"beforeYesterday\".localized\n        case .beforeOneMonth:\n            return \"beforeAMonth\".localized\n        }\n    }\n    \n    var startDate: Date {\n        switch self {\n        case .lastHour:\n            return Date.lastHour\n        case .today:\n            return Date().startOfDay\n        case .todayAndYesterday:\n            return Date.yesterday\n        case .lastMonth:\n            return Date.lastMonth\n        case .allTime,\n             .beforeOneHour,\n             .beforeToday,\n             .beforeYesterday,\n             .beforeOneMonth:\n            return Date(timeIntervalSince1970: 0)\n        }\n    }\n\n    var endDate: Date {\n        switch self {\n        case .lastHour,\n             .today,\n             .todayAndYesterday,\n             .lastMonth,\n             .allTime:\n            return Date()\n        case .beforeOneHour:\n            return Date.lastHour\n        case .beforeToday:\n            return Date().startOfDay\n        case .beforeYesterday:\n            return Date.yesterday\n        case .beforeOneMonth:\n            return Date.lastMonth\n        }\n    }\n}\n"
  },
  {
    "path": "Model/MessageItemModel.swift",
    "content": "//\n//  MessageItemModel.swift\n//  Bark\n//\n//  Created by huangfeng on 12/27/24.\n//  Copyright © 2024 Fin. All rights reserved.\n//\n\nimport Kingfisher\nimport UIKit\n\nenum MessageListCellDateStyle {\n    /// 相对时间，例如 1分钟前、1小时前\n    case relative\n    /// 精确时间，例如 2024-01-01 12:00\n    case exact\n}\n\nclass MessageItemModel {\n    var id: String = \"\"\n    var group: String?\n    \n    var attributedText: NSAttributedString?\n    var dateText: String?\n    \n    var image: String?\n    \n    var createDate: Date?\n    var dateStyle: MessageListCellDateStyle = .relative {\n        didSet {\n            switch dateStyle {\n            case .relative:\n                dateText = createDate?.agoFormatString()\n            case .exact:\n                dateText = createDate?.formatString(format: \"yyyy-MM-dd HH:mm\")\n            }\n        }\n    }\n\n    init(message: Message) {\n        self.id = message.id\n        self.group = message.group\n        \n        let title = message.title ?? \"\"\n        let subtitle = message.subtitle ?? \"\"\n        let body = message.body ?? \"\"\n        let url = message.url ?? \"\"\n        \n        let text: NSMutableAttributedString\n        if message.type == .markdown {\n            text = NSMutableAttributedString(attributedString: MarkdownParser().parse(body))\n        } else {\n            text = NSMutableAttributedString(\n                string: body,\n                attributes: [.font: UIFont.preferredFont(ofSize: 14), .foregroundColor: BKColor.grey.darken4]\n            )\n        }\n        \n        if subtitle.count > 0 {\n            // 插入一行空行当 spacer\n            text.insert(NSAttributedString(\n                string: \"\\n\",\n                attributes: [.font: UIFont.systemFont(ofSize: 6, weight: .medium)]\n            ), at: 0)\n            \n            text.insert(NSAttributedString(\n                string: subtitle + \"\\n\",\n                attributes: [.font: UIFont.preferredFont(ofSize: 16, weight: .medium), .foregroundColor: BKColor.grey.darken4]\n            ), at: 0)\n        }\n        \n        if title.count > 0 {\n            // 插入一行空行当 spacer\n            text.insert(NSAttributedString(\n                string: \"\\n\",\n                attributes: [.font: UIFont.systemFont(ofSize: 6, weight: .medium)]\n            ), at: 0)\n            \n            text.insert(NSAttributedString(\n                string: title + \"\\n\",\n                attributes: [.font: UIFont.preferredFont(ofSize: 16, weight: .medium), .foregroundColor: BKColor.grey.darken4]\n            ), at: 0)\n        }\n        \n        if url.count > 0 {\n            // 插入一行空行当 spacer\n            text.append(NSAttributedString(\n                string: \"\\n \",\n                attributes: [.font: UIFont.systemFont(ofSize: 8, weight: .medium)]\n            ))\n            \n            text.append(NSAttributedString(string: \"\\n\\(url)\", attributes: [\n                .font: UIFont.preferredFont(ofSize: 14),\n                .foregroundColor: BKColor.grey.darken4,\n                .link: url\n            ]))\n        }\n        \n        self.attributedText = text\n        self.createDate = message.createDate\n        self.image = message.image\n        defer {\n            self.dateStyle = .relative\n        }\n    }\n}\n"
  },
  {
    "path": "Model/MessageSection.swift",
    "content": "//\n//  MessageSection.swift\n//  Bark\n//\n//  Created by huangfeng on 2020/11/21.\n//  Copyright © 2020 Fin. All rights reserved.\n//\n\nimport Differentiator\nimport Foundation\nimport RxCocoa\nimport RxDataSources\n\nenum MessageListCellItem: Equatable {\n    /// 单条消息\n    case message(model: MessageItemModel)\n    /// 一组消息，可以收缩折叠\n    case messageGroup(name: String, totalCount: Int, messages: [MessageItemModel])\n    \n    // 确定 cell 内部是否需要更新\n    static func == (lhs: Self, rhs: Self) -> Bool {\n        switch (lhs, rhs) {\n        case (.message(let l), .message(let r)):\n            return l.id == r.id && l.dateText == r.dateText\n        case (.messageGroup(let l, _, let lMessages), .messageGroup(let r, _, let rMessages)):\n            if l != r {\n                return false\n            }\n            if lMessages.first?.dateText != rMessages.first?.dateText {\n                return false\n            }\n            if lMessages.count != rMessages.count {\n                return false\n            }\n            for (lMessage, rMessage) in zip(lMessages, rMessages) {\n                if lMessage.id != rMessage.id {\n                    return false\n                }\n            }\n            return true\n        default:\n            return false\n        }\n    }\n}\n\nextension MessageListCellItem: IdentifiableType {\n    typealias Identity = String\n    \n    // 确定整个 cell 是否删除或替换\n    var identity: String {\n        switch self {\n        case .message(let model):\n            return \"list_\\(model.id)_\\(model.createDate?.timeInterval ?? 0)\"\n        case .messageGroup(_, _, let messages):\n            return \"group_\\(messages.first?.group ?? \"default\".localized)_\\(messages.first?.createDate?.timeInterval ?? 0)\"\n        }\n    }\n}\n\nstruct MessageSection {\n    var header: String\n    var messages: [MessageListCellItem]\n}\n\nextension MessageSection: AnimatableSectionModelType {\n    typealias Item = MessageListCellItem\n    typealias Identity = String\n    \n    var items: [MessageListCellItem] {\n        return self.messages\n    }\n    \n    init(original: MessageSection, items: [MessageListCellItem]) {\n        self = original\n        self.messages = items\n    }\n    \n    var identity: String {\n        return header\n    }\n}\n"
  },
  {
    "path": "Model/Object+Dictionary.swift",
    "content": "//\n//  Object+Dictionary.swift\n//  Bark\n//\n//  Created by huangfeng on 2022/10/20.\n//  Copyright © 2022 Fin. All rights reserved.\n//\n\nimport Foundation\nimport RealmSwift\n\nextension Object {\n    func toDictionary() -> [String: AnyObject] {\n        var dicProps = [String: AnyObject]()\n        self.objectSchema.properties.forEach { property in\n            if property.isArray {\n                var arr: [[String: AnyObject]] = []\n                for obj in self.dynamicList(property.name) {\n                    arr.append(obj.toDictionary())\n                }\n                dicProps[property.name] = arr as AnyObject\n            } else if let value = self[property.name] as? Object {\n                dicProps[property.name] = value.toDictionary() as AnyObject\n            } else if let value = self[property.name] as? Date {\n                dicProps[property.name] = Int64(value.timeIntervalSince1970) as AnyObject\n            } else {\n                let value = self[property.name]\n                dicProps[property.name] = value as AnyObject\n            }\n        }\n        return dicProps\n    }\n}\n"
  },
  {
    "path": "Model/PreviewModel.swift",
    "content": "//\n//  PreviewModel.swift\n//  Bark\n//\n//  Created by huangfeng on 2020/11/23.\n//  Copyright © 2020 Fin. All rights reserved.\n//\n\nimport Foundation\nimport UIKit\n\nclass PreviewModel: NSObject {\n    var title: String?\n    var body: String?\n    var notice: String?\n    var queryParameter: String?\n    var image: UIImage?\n    var moreInfo: String?\n    var moreViewModel: ViewModel?\n\n    init(title: String? = nil,\n         body: String? = nil,\n         notice: String? = nil,\n         queryParameter: String? = nil,\n         image: UIImage? = nil,\n         moreInfo: String? = nil,\n         moreViewModel: ViewModel? = nil)\n    {\n        self.title = title\n        self.body = body\n        self.notice = notice\n        self.queryParameter = queryParameter\n        self.image = image\n        self.moreInfo = moreInfo\n        self.moreViewModel = moreViewModel\n    }\n}\n"
  },
  {
    "path": "NotificationServiceExtension/Info.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>CFBundleDevelopmentRegion</key>\n\t<string>$(DEVELOPMENT_LANGUAGE)</string>\n\t<key>CFBundleDisplayName</key>\n\t<string>NotificationServiceExtension</string>\n\t<key>CFBundleExecutable</key>\n\t<string>$(EXECUTABLE_NAME)</string>\n\t<key>CFBundleIdentifier</key>\n\t<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>\n\t<key>CFBundleInfoDictionaryVersion</key>\n\t<string>6.0</string>\n\t<key>CFBundleName</key>\n\t<string>$(PRODUCT_NAME)</string>\n\t<key>CFBundlePackageType</key>\n\t<string>XPC!</string>\n\t<key>CFBundleShortVersionString</key>\n\t<string>$(MARKETING_VERSION)</string>\n\t<key>CFBundleVersion</key>\n\t<string>$(CURRENT_PROJECT_VERSION)</string>\n\t<key>NSExtension</key>\n\t<dict>\n\t\t<key>NSExtensionAttributes</key>\n\t\t<dict>\n\t\t\t<key>IntentsSupported</key>\n\t\t\t<array>\n\t\t\t\t<string>INSendMessageIntent</string>\n\t\t\t</array>\n\t\t</dict>\n\t\t<key>NSExtensionPointIdentifier</key>\n\t\t<string>com.apple.usernotifications.service</string>\n\t\t<key>NSExtensionPrincipalClass</key>\n\t\t<string>$(PRODUCT_MODULE_NAME).NotificationService</string>\n\t</dict>\n</dict>\n</plist>\n"
  },
  {
    "path": "NotificationServiceExtension/NotificationService.swift",
    "content": "//\n//  NotificationService.swift\n//  NotificationServiceExtension\n//\n//  Created by huangfeng on 2018/12/17.\n//  Copyright © 2018 Fin. All rights reserved.\n//\n\nimport UserNotifications\n\nclass NotificationService: UNNotificationServiceExtension {\n    /// 当前 ContentHandler，主要用来 serviceExtensionTimeWillExpire 时交付推送\n    var currentContentHandler: ((UNNotificationContent) -> Void)? = nil\n    /// 当前正在处理的推送内容\n    var currentBestAttemptContent: UNMutableNotificationContent? = nil\n    \n    override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {\n        Task {\n            guard var bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent) else {\n                contentHandler(request.content)\n                return\n            }\n            self.currentContentHandler = contentHandler\n            \n            // 所有的 processor 按顺序从上往下对推送进行处理\n            // ciphertext 需要放在最前面，有可能所有的推送数据都在密文里\n            // icon 放在最后面，游戏模式下可能会超时，超时后后面的 processor 就没机会运行了。\n            let processors: [NotificationContentProcessorItem] = [\n                .ciphertext,\n                .markdown,\n                .level,\n                .badge,\n                .autoCopy,\n                .archive,\n                .mute,\n                .call,\n                .setImage,\n                .setIcon\n            ]\n            \n            // 各个 processor 依次对推送进行处理\n            for processor in processors.map({ $0.processor }) {\n                do {\n                    bestAttemptContent = try await processor.process(identifier: request.identifier, content: bestAttemptContent)\n                    self.currentBestAttemptContent = bestAttemptContent\n                } catch NotificationContentProcessorError.error(let content) {\n                    contentHandler(content)\n                    return\n                }\n            }\n            \n            // 处理完后交付推送\n            contentHandler(bestAttemptContent)\n            \n            // 发送 Darwin Notification 通知主 APP 有新消息\n            CFNotificationCenterPostNotification(\n                CFNotificationCenterGetDarwinNotifyCenter(),\n                CFNotificationName(\"com.bark.newmessage\" as CFString),\n                nil,\n                nil,\n                true\n            )\n        }\n    }\n\n    override func serviceExtensionTimeWillExpire() {\n        super.serviceExtensionTimeWillExpire()\n        guard let contentHandler = currentContentHandler,\n              let bestAttemptContent = currentBestAttemptContent\n        else {\n            return\n        }\n        contentHandler(bestAttemptContent)\n    }\n}\n"
  },
  {
    "path": "NotificationServiceExtension/NotificationServiceExtension.entitlements",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>com.apple.security.application-groups</key>\n\t<array>\n\t\t<string>group.bark</string>\n\t</array>\n</dict>\n</plist>\n"
  },
  {
    "path": "NotificationServiceExtension/Processor/ArchiveProcessor.swift",
    "content": "//\n//  ArchiveProcessor.swift\n//  NotificationServiceExtension\n//\n//  Created by huangfeng on 2024/5/29.\n//  Copyright © 2024 Fin. All rights reserved.\n//\n\nimport CryptoKit\nimport Foundation\n\nclass ArchiveProcessor: NotificationContentProcessor {\n    func process(identifier: String, content bestAttemptContent: UNMutableNotificationContent) async throws -> UNMutableNotificationContent {\n        let userInfo = bestAttemptContent.userInfo\n        \n        var isArchive: Bool = ArchiveSettingManager.shared.isArchive\n        if let archive = userInfo[\"isarchive\"] as? String {\n            isArchive = archive == \"1\" ? true : false\n        }\n        \n        if isArchive {\n            let alert = (userInfo[\"aps\"] as? [String: Any])?[\"alert\"] as? [String: Any]\n            let title = alert?[\"title\"] as? String\n            let subtitle = alert?[\"subtitle\"] as? String\n            let body = alert?[\"body\"] as? String\n            let url = userInfo[\"url\"] as? String\n            let group = userInfo[\"group\"] as? String\n            let image = userInfo[\"image\"] as? String\n            let id = userInfo[\"id\"] as? String\n            let markdown = userInfo[\"markdown\"] as? String\n\n            // 准备消息数据字典\n            var messageDict: [String: Any] = [:]\n            \n            let messageId = (id != nil && !id!.isEmpty) ? id! : UUID().uuidString\n            messageDict[\"id\"] = messageId\n            \n            if let title = title {\n                messageDict[\"title\"] = title\n            }\n            if let subtitle = subtitle {\n                messageDict[\"subtitle\"] = subtitle\n            }\n            if let markdown = markdown, !markdown.isEmpty {\n                messageDict[\"body\"] = markdown\n                messageDict[\"bodyType\"] = \"markdown\"\n            } else if let body = body {\n                messageDict[\"body\"] = body\n            }\n            if let url = url {\n                messageDict[\"url\"] = url\n            }\n            if let image = image {\n                messageDict[\"image\"] = image\n            }\n            if let group = group {\n                messageDict[\"group\"] = group\n            }\n            messageDict[\"createDate\"] = Date().timeIntervalSince1970\n            \n            // 写入 plist 文件\n            if let groupUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: \"group.bark\") {\n                let pendingMessagesDir = groupUrl.appendingPathComponent(\"pending_messages\")\n                \n                // 创建目录（如果不存在）\n                try? FileManager.default.createDirectory(at: pendingMessagesDir, withIntermediateDirectories: true, attributes: nil)\n                \n                        \n                // 使用 SHA256 hash 作为安全的文件名\n                let safeFilename: String\n                if let data = messageId.data(using: .utf8) {\n                    let hash = SHA256.hash(data: data)\n                    safeFilename = hash.compactMap { String(format: \"%02x\", $0) }.joined()\n                } else {\n                    safeFilename = UUID().uuidString\n                }\n\n                let plistUrl = pendingMessagesDir.appendingPathComponent(\"\\(safeFilename).plist\")\n                let dict = NSDictionary(dictionary: messageDict)\n                dict.write(to: plistUrl, atomically: true)\n            }\n        }\n        return bestAttemptContent\n    }\n}\n"
  },
  {
    "path": "NotificationServiceExtension/Processor/AutoCopyProcessor.swift",
    "content": "//\n//  AutoCopyProcessor.swift\n//  NotificationServiceExtension\n//\n//  Created by huangfeng on 2024/5/29.\n//  Copyright © 2024 Fin. All rights reserved.\n//\n\nimport Foundation\n\nclass AutoCopyProcessor: NotificationContentProcessor {\n    func process(identifier: String, content bestAttemptContent: UNMutableNotificationContent) async throws -> UNMutableNotificationContent {\n        let userInfo = bestAttemptContent.userInfo\n        if userInfo[\"autocopy\"] as? String == \"1\"\n            || userInfo[\"automaticallycopy\"] as? String == \"1\"\n        {\n            if let copy = userInfo[\"copy\"] as? String {\n                UIPasteboard.general.string = copy\n            } else {\n                UIPasteboard.general.string = bestAttemptContent.bodyText\n            }\n        }\n        return bestAttemptContent\n    }\n}\n"
  },
  {
    "path": "NotificationServiceExtension/Processor/BadgeProcessor.swift",
    "content": "//\n//  BadgeProcessor.swift\n//  NotificationServiceExtension\n//\n//  Created by huangfeng on 2024/5/29.\n//  Copyright © 2024 Fin. All rights reserved.\n//\n\nimport Foundation\n\n/// 通知角标\nclass BadgeProcessor: NotificationContentProcessor {\n    func process(identifier: String, content bestAttemptContent: UNMutableNotificationContent) async throws -> UNMutableNotificationContent {\n        if let badgeStr = bestAttemptContent.userInfo[\"badge\"] as? String, let badge = Int(badgeStr) {\n            bestAttemptContent.badge = NSNumber(value: badge)\n        }\n        return bestAttemptContent\n    }\n}\n"
  },
  {
    "path": "NotificationServiceExtension/Processor/CallProcessor.swift",
    "content": "//\n//  CallProcessor.swift\n//  NotificationServiceExtension\n//\n//  Created by huangfeng on 2024/6/6.\n//  Copyright © 2024 Fin. All rights reserved.\n//\n\nimport AudioToolbox\nimport AVFAudio\nimport Foundation\n\nclass CallProcessor: NotificationContentProcessor {\n    /// 铃声文件夹，扩展访问不到主APP中的铃声，需要先共享铃声文件\n    let soundsDirectoryUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: \"group.bark\")?.appendingPathComponent(\"Library/Sounds\")\n    \n    func process(identifier: String, content bestAttemptContent: UNMutableNotificationContent) async throws -> UNMutableNotificationContent {\n        guard let call = bestAttemptContent.userInfo[\"call\"] as? String, call == \"1\" else {\n            return bestAttemptContent\n        }\n        // 延长铃声到30s\n        return self.processNotificationSound(content: bestAttemptContent)\n    }\n}\n\n// MARK: - 铃声\n\nextension CallProcessor {\n    // 将通知铃声延长到30s，并用30s的长铃声替换掉原铃声\n    func processNotificationSound(content: UNMutableNotificationContent) -> UNMutableNotificationContent {\n        let sound = ((content.userInfo[\"aps\"] as? [String: Any])?[\"sound\"] as? String)?.split(separator: \".\")\n        let soundName: String\n        let soundType: String\n        if sound?.count == 2, let first = sound?.first, let last = sound?.last, last == \"caf\" {\n            soundName = String(first)\n            soundType = String(last)\n        } else {\n            soundName = \"multiwayinvitation\"\n            soundType = \"caf\"\n        }\n        \n        if let longSoundUrl = getLongSound(soundName: soundName, soundType: soundType) {\n            if content.isCritical {\n                LevelProcessor.setCriticalSound(content: content, soundName: longSoundUrl.lastPathComponent)\n            } else {\n                content.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: longSoundUrl.lastPathComponent))\n            }\n        }\n        return content\n    }\n    \n    func getLongSound(soundName: String, soundType: String) -> URL? {\n        guard let soundsDirectoryUrl else {\n            return nil\n        }\n        // 创建铃声文件夹\n        if !FileManager.default.fileExists(atPath: soundsDirectoryUrl.path) {\n            try? FileManager.default.createDirectory(atPath: soundsDirectoryUrl.path, withIntermediateDirectories: true, attributes: nil)\n        }\n        \n        // 已经存在处理过的长铃声，则直接返回\n        let longSoundName = \"\\(kBarkSoundPrefix).\\(soundName).\\(soundType)\"\n        let longSoundPath = soundsDirectoryUrl.appendingPathComponent(longSoundName)\n        if FileManager.default.fileExists(atPath: longSoundPath.path) {\n            return longSoundPath\n        }\n        \n        // 原始铃声路径\n        var path: String = soundsDirectoryUrl.appendingPathComponent(\"\\(soundName).\\(soundType)\").path\n        if !FileManager.default.fileExists(atPath: path) {\n            // 不存在自定义的铃声，就用内置的铃声\n            path = Bundle.main.path(forResource: soundName, ofType: soundType) ?? \"\"\n        }\n        guard !path.isEmpty else {\n            return nil\n        }\n        \n        // 将原始铃声处理成30s的长铃声，并缓存起来\n        return mergeCAFFilesToDuration(inputFile: URL(fileURLWithPath: path))\n    }\n\n    /// - Author: @uuneo\n    /// - Description:将输入的音频文件重复为指定时长的音频文件\n    /// - Parameters:\n    ///   - inputFile: 原始铃声文件路径\n    ///   - targetDuration: 重复的时长\n    /// - Returns: 长铃声文件路径\n    func mergeCAFFilesToDuration(inputFile: URL, targetDuration: TimeInterval = 30) -> URL? {\n        guard let soundsDirectoryUrl else {\n            return nil\n        }\n        let longSoundName = \"\\(kBarkSoundPrefix).\\(inputFile.lastPathComponent)\"\n        let longSoundPath = soundsDirectoryUrl.appendingPathComponent(longSoundName)\n        \n        do {\n            // 打开输入文件并获取音频格式\n            let audioFile = try AVAudioFile(forReading: inputFile)\n            let audioFormat = audioFile.processingFormat\n            let sampleRate = audioFormat.sampleRate\n\n            // 计算目标帧数\n            let targetFrames = AVAudioFramePosition(targetDuration * sampleRate)\n            var currentFrames: AVAudioFramePosition = 0\n            // 创建输出音频文件\n            let outputAudioFile = try AVAudioFile(forWriting: longSoundPath, settings: audioFormat.settings)\n            \n            // 循环读取文件数据，拼接到目标时长\n            while currentFrames < targetFrames {\n                // 每次读取整个文件的音频数据\n                guard let buffer = AVAudioPCMBuffer(pcmFormat: audioFormat, frameCapacity: AVAudioFrameCount(audioFile.length)) else {\n                    // 出错了就终止，避免死循环\n                    return nil\n                }\n                \n                try audioFile.read(into: buffer)\n\n                // 计算剩余所需帧数\n                let remainingFrames = targetFrames - currentFrames\n                if AVAudioFramePosition(buffer.frameLength) > remainingFrames {\n                    // 如果当前缓冲区帧数超出所需，截取剩余部分\n                    let truncatedBuffer = AVAudioPCMBuffer(pcmFormat: buffer.format, frameCapacity: AVAudioFrameCount(remainingFrames))!\n                    let channelCount = Int(buffer.format.channelCount)\n                    for channel in 0..<channelCount {\n                        let sourcePointer = buffer.floatChannelData![channel]\n                        let destinationPointer = truncatedBuffer.floatChannelData![channel]\n                        memcpy(destinationPointer, sourcePointer, Int(remainingFrames) * MemoryLayout<Float>.size)\n                    }\n                    truncatedBuffer.frameLength = AVAudioFrameCount(remainingFrames)\n                    try outputAudioFile.write(from: truncatedBuffer)\n                    break\n                } else {\n                    // 否则写入整个缓冲区\n                    try outputAudioFile.write(from: buffer)\n                    currentFrames += AVAudioFramePosition(buffer.frameLength)\n                }\n                \n                // 重置输入文件读取位置\n                audioFile.framePosition = 0\n            }\n            return longSoundPath\n        } catch {\n            print(\"Error processing CAF file: \\(error)\")\n            return nil\n        }\n    }\n}\n"
  },
  {
    "path": "NotificationServiceExtension/Processor/CiphertextProcessor.swift",
    "content": "//\n//  CiphertextProcessor.swift\n//  NotificationServiceExtension\n//\n//  Created by huangfeng on 2024/5/29.\n//  Copyright © 2024 Fin. All rights reserved.\n//\n\nimport Foundation\nimport SwiftyJSON\n\n/// 加密推送\nclass CiphertextProcessor: NotificationContentProcessor {\n    func process(identifier: String, content bestAttemptContent: UNMutableNotificationContent) async throws -> UNMutableNotificationContent {\n        var userInfo = bestAttemptContent.userInfo\n        guard let ciphertext = userInfo[\"ciphertext\"] as? String else {\n            return bestAttemptContent\n        }\n        \n        // 如果是加密推送，则使用密文配置 bestAttemptContent\n        do {\n            var map = try decrypt(ciphertext: ciphertext, iv: userInfo[\"iv\"] as? String)\n            \n            var alert = [String: Any]()\n            var soundName: String? = nil\n            if let title = map[\"title\"] as? String {\n                bestAttemptContent.title = title\n                alert[\"title\"] = title\n            }\n            if let subtitle = map[\"subtitle\"] as? String {\n                bestAttemptContent.subtitle = subtitle\n                alert[\"subtitle\"] = subtitle\n            }\n            if let body = map[\"body\"] as? String {\n                bestAttemptContent.body = body\n                alert[\"body\"] = body\n            }\n            if let group = map[\"group\"] as? String {\n                bestAttemptContent.threadIdentifier = group\n            }\n            if var sound = map[\"sound\"] as? String {\n                if !sound.hasSuffix(\".caf\") {\n                    sound = \"\\(sound).caf\"\n                }\n                soundName = sound\n                bestAttemptContent.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: sound))\n            }\n            if let badge = map[\"badge\"] as? String, let badgeValue = Int(badge) {\n                bestAttemptContent.badge = badgeValue as NSNumber\n            }\n            var aps: [String: Any] = [\"alert\": alert]\n            if let soundName {\n                aps[\"sound\"] = soundName\n            }\n            map[\"aps\"] = aps\n        \n            userInfo = map\n            bestAttemptContent.userInfo = userInfo\n            return bestAttemptContent\n        } catch {\n            bestAttemptContent.body = \"Decryption Failed\"\n            bestAttemptContent.userInfo = [\"aps\": [\"alert\": [\"body\": bestAttemptContent.body]]]\n            throw NotificationContentProcessorError.error(content: bestAttemptContent)\n        }\n    }\n    \n    /// 解密文本\n    /// - Parameters:\n    ///   - ciphertext: 密文\n    ///   - iv: iv 如果不传就用配置保存的，传了就以传的 iv 为准\n    /// - Returns: 解密后的 json 数据\n    private func decrypt(ciphertext: String, iv: String? = nil) throws -> [AnyHashable: Any] {\n        guard var fields = CryptoSettingManager.shared.fields else {\n            throw \"No encryption key set\"\n        }\n        if let iv = iv {\n            // Support using specified IV parameter for decryption\n            fields.iv = iv\n        }\n        \n        let aes = try AESCryptoModel(cryptoFields: fields)\n        \n        let json = try aes.decrypt(ciphertext: ciphertext)\n        \n        guard let data = json.data(using: .utf8), let map = JSON(data).dictionaryObject else {\n            throw \"JSON parsing failed\"\n        }\n        \n        var result: [AnyHashable: Any] = [:]\n        for (key, val) in map {\n            // 将key重写为小写, 防止用户误输入大小写，全按小写处理\n            let key = key.lowercased()\n            // 将 value 全部转换成字符串，因为其他方式传参的结果都是字符串\n            var val = val\n            \n            // 如果是数字，转成字符串\n            if let num = val as? NSNumber {\n                val = num.stringValue\n            }\n            \n            result[key] = val\n        }\n        return result\n    }\n}\n"
  },
  {
    "path": "NotificationServiceExtension/Processor/IconProcessor.swift",
    "content": "//\n//  IconProcessor.swift\n//  NotificationServiceExtension\n//\n//  Created by huangfeng on 2024/5/29.\n//  Copyright © 2024 Fin. All rights reserved.\n//\n\nimport Foundation\nimport Intents\n\nclass IconProcessor: NotificationContentProcessor {\n    func process(identifier: String, content bestAttemptContent: UNMutableNotificationContent) async throws -> UNMutableNotificationContent {\n        if #available(iOSApplicationExtension 15.0, *) {\n            let userInfo = bestAttemptContent.userInfo\n            \n            guard let imageUrl = userInfo[\"icon\"] as? String,\n                  let imageFileUrl = await ImageDownloader.downloadImage(imageUrl),\n                  let imageData = NSData(contentsOfFile: imageFileUrl)\n            else {\n                return bestAttemptContent\n            }\n            \n            var personNameComponents = PersonNameComponents()\n            personNameComponents.nickname = bestAttemptContent.title\n            \n            let avatar = INImage(imageData: imageData as Data)\n            let senderPerson = INPerson(\n                personHandle: INPersonHandle(value: \"\", type: .unknown),\n                nameComponents: personNameComponents,\n                displayName: personNameComponents.nickname,\n                image: avatar,\n                contactIdentifier: nil,\n                customIdentifier: nil,\n                isMe: false,\n                suggestionType: .none\n            )\n            let mePerson = INPerson(\n                personHandle: INPersonHandle(value: \"\", type: .unknown),\n                nameComponents: nil,\n                displayName: nil,\n                image: nil,\n                contactIdentifier: nil,\n                customIdentifier: nil,\n                isMe: true,\n                suggestionType: .none\n            )\n            \n            // 必须两个接受者，才能显示 subtitle, 别问为什么\n            let placeholderPerson = INPerson(\n                personHandle: INPersonHandle(value: \"\", type: .unknown),\n                nameComponents: personNameComponents,\n                displayName: personNameComponents.nickname,\n                image: avatar,\n                contactIdentifier: nil,\n                customIdentifier: nil\n            )\n            \n            let intent = INSendMessageIntent(\n                recipients: [mePerson, placeholderPerson],\n                outgoingMessageType: .outgoingMessageText,\n                content: bestAttemptContent.body,\n                speakableGroupName: INSpeakableString(spokenPhrase: bestAttemptContent.subtitle),\n                conversationIdentifier: bestAttemptContent.threadIdentifier,\n                serviceName: nil,\n                sender: senderPerson,\n                attachments: nil\n            )\n            \n            intent.setImage(avatar, forParameterNamed: \\.speakableGroupName)\n            \n            let interaction = INInteraction(intent: intent, response: nil)\n            interaction.direction = .incoming\n            \n            do {\n                try await interaction.donate()\n                let content = try bestAttemptContent.updating(from: intent) as! UNMutableNotificationContent\n                return content\n            } catch {\n                return bestAttemptContent\n            }\n        } else {\n            return bestAttemptContent\n        }\n    }\n}\n"
  },
  {
    "path": "NotificationServiceExtension/Processor/ImageDownloader.swift",
    "content": "//\n//  ImageDownloader.swift\n//  NotificationServiceExtension\n//\n//  Created by huangfeng on 2024/5/29.\n//  Copyright © 2024 Fin. All rights reserved.\n//\n\nimport Foundation\nimport Kingfisher\n\nclass ImageDownloader {\n    /// 保存图片到缓存中\n    /// - Parameters:\n    ///   - cache: 使用的缓存\n    ///   - data: 图片 Data 数据\n    ///   - key: 缓存 Key\n    class func storeImage(cache: ImageCache, data: Data, key: String) async {\n        return await withCheckedContinuation { continuation in\n            cache.storeToDisk(data, forKey: key, expiration: StorageExpiration.never) { _ in\n                continuation.resume()\n            }\n        }\n    }\n    \n    /// 使用 Kingfisher.ImageDownloader 下载图片\n    /// - Parameter url: 下载的图片URL\n    /// - Returns: 返回 Result\n    class func downloadImage(url: URL) async -> Result<ImageLoadingResult, KingfisherError> {\n        return await withCheckedContinuation { continuation in\n            let downloader = Kingfisher.ImageDownloader.default\n            downloader.downloadTimeout = 10.0\n            downloader.downloadImage(with: url, options: nil) { result in\n                continuation.resume(returning: result)\n            }\n        }\n    }\n   \n    /// 下载推送图片\n    /// - Parameter imageUrl: 图片URL字符串\n    /// - Returns: 保存在本地中的`图片 File URL`\n    class func downloadImage(_ imageUrl: String) async -> String? {\n        guard let groupUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: \"group.bark\"),\n              let cache = try? ImageCache(name: \"shared\", cacheDirectoryURL: groupUrl),\n              let imageResource = URL(string: imageUrl)\n        else {\n            return nil\n        }\n        \n        // 先查看图片缓存\n        if cache.diskStorage.isCached(forKey: imageResource.cacheKey) {\n            return cache.cachePath(forKey: imageResource.cacheKey)\n        }\n        \n        // 下载图片\n        guard let result = try? await downloadImage(url: imageResource).get() else {\n            return nil\n        }\n        // 缓存图片\n        await storeImage(cache: cache, data: result.originalData, key: imageResource.cacheKey)\n        \n        return cache.cachePath(forKey: imageResource.cacheKey)\n    }\n}\n"
  },
  {
    "path": "NotificationServiceExtension/Processor/ImageProcessor.swift",
    "content": "//\n//  ImageProcessor.swift\n//  NotificationServiceExtension\n//\n//  Created by huangfeng on 2024/5/29.\n//  Copyright © 2024 Fin. All rights reserved.\n//\n\nimport Foundation\nimport MobileCoreServices\n\nclass ImageProcessor: NotificationContentProcessor {\n    func process(identifier: String, content bestAttemptContent: UNMutableNotificationContent) async throws -> UNMutableNotificationContent {\n        let userInfo = bestAttemptContent.userInfo\n        guard let imageUrl = userInfo[\"image\"] as? String,\n              let imageFileUrl = await ImageDownloader.downloadImage(imageUrl)\n        else {\n            return bestAttemptContent\n        }\n        \n        let copyDestUrl = URL(fileURLWithPath: imageFileUrl).appendingPathExtension(\".tmp\")\n        // 将图片缓存复制一份，推送使用完后会自动删除，但图片缓存需要留着以后在历史记录里查看\n        try? FileManager.default.copyItem(\n            at: URL(fileURLWithPath: imageFileUrl),\n            to: copyDestUrl\n        )\n        \n        if let attachment = try? UNNotificationAttachment(\n            identifier: \"image\",\n            url: copyDestUrl,\n            options: [UNNotificationAttachmentOptionsTypeHintKey: kUTTypePNG]\n        ) {\n            bestAttemptContent.attachments = [attachment]\n        }\n        return bestAttemptContent\n    }\n}\n"
  },
  {
    "path": "NotificationServiceExtension/Processor/LevelProcessor.swift",
    "content": "//\n//  LevelProcessor.swift\n//  NotificationServiceExtension\n//\n//  Created by huangfeng on 2024/5/29.\n//  Copyright © 2024 Fin. All rights reserved.\n//\n\nimport Foundation\n\n/// 通知中断级别\nclass LevelProcessor: NotificationContentProcessor {\n    func process(identifier: String, content bestAttemptContent: UNMutableNotificationContent) async throws -> UNMutableNotificationContent {\n        guard let level = bestAttemptContent.userInfo[\"level\"] as? String else {\n            return bestAttemptContent\n        }\n        \n        if bestAttemptContent.isCritical {\n            // 设置重要警告音效\n            LevelProcessor.setCriticalSound(content: bestAttemptContent)\n            return bestAttemptContent\n        }\n        \n        // 其他的，例如时效性通知\n        guard #available(iOSApplicationExtension 15.0, *) else {\n            return bestAttemptContent\n        }\n        \n        let interruptionLevels: [String: UNNotificationInterruptionLevel] = [\n            \"passive\": UNNotificationInterruptionLevel.passive,\n            \"active\": UNNotificationInterruptionLevel.active,\n            \"timeSensitive\": UNNotificationInterruptionLevel.timeSensitive,\n            \"timesensitive\": UNNotificationInterruptionLevel.timeSensitive\n        ]\n        bestAttemptContent.interruptionLevel = interruptionLevels[level] ?? .active\n        return bestAttemptContent\n    }\n}\n\nextension LevelProcessor {\n    class func setCriticalSound(content bestAttemptContent: UNMutableNotificationContent, soundName: String? = nil) {\n        guard bestAttemptContent.isCritical else {\n            return\n        }\n        // 默认音量\n        var audioVolume: Float = 0.5\n        // 指定音量，取值范围是 0 - 10 , 会转换成 0.0 - 1.0\n        if let volume = bestAttemptContent.userInfo[\"volume\"] as? String, let volume = Float(volume) {\n            audioVolume = max(0.0, min(1, volume / 10.0))\n        }\n        // 设置重要警告 sound\n        let sound = soundName ?? bestAttemptContent.soundName\n        if let sound {\n            bestAttemptContent.sound = UNNotificationSound.criticalSoundNamed(UNNotificationSoundName(rawValue: sound), withAudioVolume: audioVolume)\n        } else {\n            bestAttemptContent.sound = UNNotificationSound.defaultCriticalSound(withAudioVolume: audioVolume)\n        }\n    }\n}\n\nextension UNMutableNotificationContent {\n    /// 是否是重要警告\n    var isCritical: Bool {\n        self.userInfo[\"level\"] as? String == \"critical\"\n    }\n\n    /// 声音名称\n    var soundName: String? {\n        (self.userInfo[\"aps\"] as? [AnyHashable: Any])?[\"sound\"] as? String\n    }\n}\n"
  },
  {
    "path": "NotificationServiceExtension/Processor/MarkdownProcessor.swift",
    "content": "//\n//  MarkdownProcessor.swift\n//  NotificationServiceExtension\n//\n//  Created by huangfeng on 11/21/25.\n//  Copyright © 2025 Fin. All rights reserved.\n//\n\nimport UIKit\n\nclass MarkdownProcessor: NotificationContentProcessor {\n    func process(identifier: String, content bestAttemptContent: UNMutableNotificationContent) async throws -> UNMutableNotificationContent {\n        let userInfo = bestAttemptContent.userInfo\n        guard let markdown = userInfo[\"markdown\"] as? String, !markdown.isEmpty else {\n            return bestAttemptContent\n        }\n        let config = MarkdownParser.Configuration(\n            baseFont: UIFont.preferredFont(forTextStyle: .body),\n            baseColor: UIColor.white,\n            linkColor: UIColor.systemBlue,\n            codeTextColor: UIColor.black,\n            codeBackgroundColor: UIColor.gray,\n            codeBlockTextColor: UIColor.black,\n            quoteColor: UIColor.systemGray\n        )\n        let body = MarkdownParser(configuration: config)\n            .parse(markdown)\n            .string\n            // 将 body 中的多个\\n替换为单个\\n，避免空行太多内容显示不完整。\n            .replacingOccurrences(of: \"\\n\\n+\", with: \"\\n\", options: .regularExpression)\n        bestAttemptContent.body = body\n        \n        /// 更新 APS 字段, 供之后的 Porgressor 使用\n        var aps = userInfo[\"aps\"] as? [String: Any] ?? [:]\n        var alert = aps[\"alert\"] as? [String: Any] ?? [:]\n        alert[\"body\"] = body\n        aps[\"alert\"] = alert\n        bestAttemptContent.userInfo[\"aps\"] = aps\n\n        return bestAttemptContent\n    }\n}\n"
  },
  {
    "path": "NotificationServiceExtension/Processor/MuteProcessor.swift",
    "content": "//\n//  MuteProcessor.swift\n//  NotificationServiceExtension\n//\n//  Created by huangfeng on 11/6/24.\n//  Copyright © 2024 Fin. All rights reserved.\n//\n\nimport UIKit\n\nclass MuteProcessor: NotificationContentProcessor {\n    func process(identifier: String, content bestAttemptContent: UNMutableNotificationContent) async throws -> UNMutableNotificationContent {\n        let groupName = bestAttemptContent.threadIdentifier\n        guard let date = GroupMuteSettingManager().settings[groupName], date > Date() else {\n            return bestAttemptContent\n        }\n        // 需要静音\n        if #available(iOSApplicationExtension 15.0, *) {\n            bestAttemptContent.interruptionLevel = .passive\n        }\n        return bestAttemptContent\n    }\n}\n"
  },
  {
    "path": "NotificationServiceExtension/Processor/NotificationContentProcessor.swift",
    "content": "//\n//  NotificationContentProcessor.swift\n//  NotificationServiceExtension\n//\n//  Created by huangfeng on 2024/5/29.\n//  Copyright © 2024 Fin. All rights reserved.\n//\n\nimport Foundation\n@_exported import UserNotifications\n\nenum NotificationContentProcessorItem {\n    case ciphertext\n    case level\n    case badge\n    case autoCopy\n    case archive\n    case setIcon\n    case setImage\n    case call\n    case mute\n    case markdown\n    \n    var processor: NotificationContentProcessor {\n        switch self {\n        case .ciphertext:\n            return CiphertextProcessor()\n        case .level:\n            return LevelProcessor()\n        case .badge:\n            return BadgeProcessor()\n        case .autoCopy:\n            return AutoCopyProcessor()\n        case .archive:\n            return ArchiveProcessor()\n        case .setIcon:\n            return IconProcessor()\n        case .setImage:\n            return ImageProcessor()\n        case .call:\n            return CallProcessor()\n        case .mute:\n            return MuteProcessor()\n        case .markdown:\n            return MarkdownProcessor()\n        }\n    }\n}\n\nenum NotificationContentProcessorError: Swift.Error {\n    case error(content: UNMutableNotificationContent)\n}\n\npublic protocol NotificationContentProcessor {\n    /// 处理 UNMutableNotificationContent\n    /// - Parameters:\n    ///   - identifier: request.identifier, 有些 Processor 需要，例如 CallProcessor 需要这个去添加 LocalNotification\n    ///   - bestAttemptContent: 需要处理的 UNMutableNotificationContent\n    /// - Returns: 处理成功后的 UNMutableNotificationContent\n    /// - Throws: 处理失败后，应该中断处理\n    func process(identifier: String, content bestAttemptContent: UNMutableNotificationContent) async throws -> UNMutableNotificationContent\n}\n"
  },
  {
    "path": "Podfile",
    "content": "source 'https://cdn.cocoapods.org/'\n\nplatform:ios,'13.0'\ninhibit_all_warnings!\nuse_modular_headers!\n\n\ndef pods\n    pod 'SnapKit'\n    pod 'Material'\n    pod 'SVProgressHUD'\n    pod 'FDFullscreenPopGesture'\n    pod 'Moya/RxSwift'\n    pod 'ObjectMapper'\n    pod 'SwiftyJSON'\n    pod 'DefaultsKit'\n    pod 'RealmSwift'\n    pod 'CryptoSwift'\n    pod 'IQKeyboardManagerSwift/IQKeyboardToolbarManager'\n    \n    pod 'RxSwift'\n    pod 'RxCocoa'\n    pod 'RxGesture'\n    pod 'RxDataSources'\n    pod 'NSObject+Rx'\n    \n    pod 'MJRefresh'\n    pod 'Kingfisher'\n    pod 'MercariQRScanner', :git => 'https://github.com/Finb/QRScanner'\n    pod 'DropDown'\n    pod 'ImageViewer.swift', :git => 'https://github.com/Finb/ImageViewer.swift'\n    \n    pod 'SwiftyStoreKit'\nend\n\ntarget 'Bark' do\n    pods\n    \n    target 'BarkTests' do\n      inherit! :search_paths\n    end\n    \nend\n\n\ntarget 'NotificationServiceExtension' do\n    pod 'RealmSwift'\n    pod 'Kingfisher'\n    pod 'CryptoSwift'\n    pod 'SwiftyJSON'\nend\n\ntarget 'NotificationContentExtension' do\n    pod 'Kingfisher'\nend\n\n\npost_install do |installer|\n    installer.pods_project.targets.each do |target|\n        target.build_configurations.each do |config|\n            if config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'].to_f < 13.0\n                config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0'\n            end\n            config.build_settings['CODE_SIGNING_ALLOWED'] = 'NO'\n        end\n    end\nend\n"
  },
  {
    "path": "README.md",
    "content": "English | **[中文](README.zh.md)**\n## Bark\nBark is a push notification tool app. It's free, simple, and secure, leveraging APNs without draining device battery.<br/>\nBark supports many advanced features of iOS notifications, including notification grouping, custom push icons, sounds, time-sensitive notifications, critical alerts, and more.<br/> \nAdditionally, Bark supports self-hosted servers and offers encrypted push notifications to ensure privacy and security. <br/>\n\n## Download\n<a target='_blank' href='https://apps.apple.com/app/bark-custom-notifications/id1403753865'>\n<img src='http://ww2.sinaimg.cn/large/0060lm7Tgw1f1hgrs1ebwj308102q0sp.jpg' width='144' height='49' />\n</a>\n\n## Documentation\n[https://bark.day.app/#/en-us/](https://bark.day.app/#/en-us/)\n\n## Feedback\n[Telegram](https://t.me/joinchat/OsCbLzovUAE0YjY1)\n\n## Usage\n1. Open the app and copy the test URL\n\n<img src=\"https://wx4.sinaimg.cn/mw2000/003rYfqply1grd1meqrvcj60bi08zt9i02.jpg\" width=365 />\n\n2. Modify the content and request this URL\n```\nYou can send GET or POST requests, and you'll receive a push notification immediately upon success.\n\nURL structure: The first part is the key, followed by three matches\n/:key/:body \n/:key/:title/:body \n/:key/:title/:subtitle/:body \n\ntitle: The push title, slightly larger than the body text \nsubtitle: The push subtitle\nbody: The push content, use the newline character '\\n' for line breaks \nFor POST requests, the parameter names are the same as above\n```\n\n## Parameters\n\n* url\n```\n// Click on the push notification to jump to the specified URL\nhttps://api.day.app/yourkey/url?url=https://www.google.com \n```\n* group\n```\n// Specify the push message group to view pushes by group.\nhttps://api.day.app/yourkey/group?group=groupName\n```\n* icon (supported on iOS 15 and above)\n```\n// Specify the push message icon\nhttps://api.day.app/yourkey/icon?icon=http://day.app/assets/images/avatar.jpg\n```\n* sound\n```\n// Specify the push message sound\nhttps://api.day.app/yourkey/sound?sound=alarm\n```\n* call\n```\n// Play sound repeatedly for 30 seconds\nhttps://api.day.app/yourkey/call?call=1\n```\n* ciphertext\n```\n// Encrypted push message\nhttps://api.day.app/yourkey/ciphertext?ciphertext=\n```\n* Time-sensitive notifications\n```\n// Set time-sensitive notifications\nhttps://api.day.app/yourkey/时效性通知?level=timeSensitive\n\n// Optional values \n// active: Default value when not set, the system will immediately display the notification by lighting up the screen. \n// timeSensitive: Time-sensitive notification, can be displayed during focus mode. \n// passive: Adds notification to the notification list without lighting up the screen.\n```\n* Critical alerts\n```\n// Set critical alerts\nhttps://api.day.app/yourkey/criticalAlert?level=critical\n\nCritical alerts will ignore silent and do not disturb modes, always playing the notification sound and displaying on the screen.\n```\n\n## Others\n- [Browser Extension](https://github.com/ij369/bark-sender)\n- [Online Scheduled Sending](https://api.ihint.me/bark.html)\n- [Windows Push Client](https://github.com/HsuDan/BarkHelper)\n- [Cross-platform Command Line Application](https://github.com/JasonkayZK/bark-cli)\n- [Bark GitHub Actions](https://github.com/harryzcy/action-bark)\n- [Quicker Actions](https://getquicker.net/Sharedaction?code=e927d844-d212-4428-758d-08d69de12a3b)\n- [Bark for Wox](https://github.com/Zeroto521/Wox.Plugin.Bark)\n- [bark-jssdk](https://github.com/afeiship/bark-jssdk)\n- [bark.js](https://github.com/skyhancloud/bark.js)\n- [java-bark-server](https://gitee.com/hotlcc/java-bark-server)\n- [bark-java-sdk](https://github.com/MoshiCoCo/bark-java-sdk)\n- [Python for Bark](https://github.com/funny-cat-happy/barknotificator)\n- [uTools for Bark](https://u.tools/plugins/detail/PushOne/)\n- [PHP for Bark](https://github.com/guanguans/notify/tree/main/src/Bark/)\n"
  },
  {
    "path": "README.zh.md",
    "content": "**[English](README.en.md)** | 中文 \n## Bark\nBark 是一款免费的推送通知工具 App。<br/>\n它简单、安全，基于 APNs 实现，不会额外消耗设备电量。<br/>\n<br/>\nBark 支持 iOS 通知的多种高级功能：推送分组、自定义图标和铃声、时效性通知、重要警告等。<br/>\n此外，Bark 还支持用户自建服务端，并提供端到端推送加密。APP 是由 Github Action 自动构建和发布，从根本上保障隐私与安全。<br/>\n\n## 下载\n<a target='_blank' href='https://apps.apple.com/app/bark-custom-notifications/id1403753865'>\n<img src='http://ww2.sinaimg.cn/large/0060lm7Tgw1f1hgrs1ebwj308102q0sp.jpg' width='144' height='49' />\n</a>\n\n## 使用文档\n[https://bark.day.app](https://bark.day.app)\n\n## 问题反馈\n[Bark反馈群](https://t.me/joinchat/OsCbLzovUAE0YjY1)\n\n## 发送推送\n1. 打开APP，复制测试URL \n\n<img src=\"https://wx4.sinaimg.cn/mw2000/003rYfqply1grd1meqrvcj60bi08zt9i02.jpg\" width=365 />\n\n2. 修改内容，请求这个URL\n```\n可以发 get 或者 post 请求 ，请求成功会立即收到推送 \n\nURL 组成: 第一个部分是 key , 之后有三个匹配 \n/:key/:body \n/:key/:title/:body \n/:key/:title/:subtitle/:body \n\ntitle 推送标题 比 body 字号粗一点 \nsubtitle 推送副标题\nbody 推送内容 换行请使用换行符 '\\n'\npost 请求 参数名也是上面这些\n```\n\n## 功能参数\n\n* url\n```\n// 点击推送将跳转到url的地址（发送时，URL参数需要编码）\nhttps://api.day.app/yourkey/百度网址?url=https://www.baidu.com \n```\n* group\n```\n// 指定推送消息分组，可在历史记录中按分组查看推送。\nhttps://api.day.app/yourkey/需要分组的推送?group=groupName\n```\n* icon (仅 iOS15 或以上支持）\n```\n// 指定推送消息图标\nhttps://api.day.app/yourkey/需要自定义图标的推送?icon=http://day.app/assets/images/avatar.jpg\n```\n* sound\n```\n// 指定推送消息的铃声\nhttps://api.day.app/yourkey/sound?sound=alarm\n```\n* call\n```\n// 重复播放铃声30s\nhttps://api.day.app/yourkey/call?call=1\n```\n* ciphertext\n```\n// 推送加密的密文\nhttps://api.day.app/yourkey/ciphertext?ciphertext=\n```\n* 时效性通知\n```\n// 设置时效性通知\nhttps://api.day.app/yourkey/时效性通知?level=timeSensitive\n\n// 可选参数值\n// active：不设置时的默认值，系统会立即亮屏显示通知。\n// timeSensitive：时效性通知，可在专注状态下显示通知。\n// passive：仅将通知添加到通知列表，不会亮屏提醒\n```\n* 重要警告\n```\n// 设置时效性通知\nhttps://api.day.app/yourkey/时效性通知?level=critical\n\n重要警告会忽略静音和勿扰模式，始终播放通知声音并在屏幕上显示。\n```\n## 其他\n- [浏览器扩展](https://github.com/ij369/bark-sender)\n- [在线定时发送](https://api.ihint.me/bark.html)\n- [Windows推送客户端](https://github.com/HsuDan/BarkHelper)\n- [跨平台的命令行应用](https://github.com/JasonkayZK/bark-cli)\n- [Bark GitHub Actions](https://github.com/harryzcy/action-bark)\n- [Quicker 动作](https://getquicker.net/Sharedaction?code=e927d844-d212-4428-758d-08d69de12a3b)\n- [Bark for Wox](https://github.com/Zeroto521/Wox.Plugin.Bark)\n- [bark-jssdk](https://github.com/afeiship/bark-jssdk)\n- [java-bark-server](https://gitee.com/hotlcc/java-bark-server)\n- [bark-java-sdk](https://github.com/MoshiCoCo/bark-java-sdk)\n- [Python for Bark](https://github.com/funny-cat-happy/barknotificator)\n- [uTools for Bark](https://u.tools/plugins/detail/PushOne/)\n- [PHP for Bark](https://github.com/guanguans/notify/tree/main/src/Bark/)\n"
  },
  {
    "path": "View/AddSoundCell.swift",
    "content": "//\n//  AddSoundCell.swift\n//  Bark\n//\n//  Created by Fin on 2024/3/29.\n//  Copyright © 2024 Fin. All rights reserved.\n//\n\nimport UIKit\n\nclass AddSoundCell: UITableViewCell {\n    let button: UIButton = {\n        let button = UIButton(type: .system)\n        button.setTitle(\"uploadSound\".localized, for: .normal)\n        button.setImage(UIImage(named: \"music_note-music_note_symbol\"), for: .normal)\n        button.setTitleColor(BKColor.lightBlue.darken3, for: .normal)\n        button.tintColor = BKColor.lightBlue.darken3\n        button.titleLabel?.font = UIFont.preferredFont(ofSize: 16)\n        button.titleLabel?.adjustsFontForContentSizeCategory = true\n        // 从 UITableView didSelectRowAt 那响应点击事件\n        button.isUserInteractionEnabled = false\n        return button\n    }()\n    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {\n        super.init(style: style, reuseIdentifier: reuseIdentifier)\n        self.selectionStyle = .none\n        contentView.addSubview(button)\n        button.snp.makeConstraints { make in\n            make.edges.equalToSuperview()\n            make.height.equalTo(44)\n        }\n    }\n    \n    @available(*, unavailable)\n    required init?(coder: NSCoder) {\n        fatalError(\"init(coder:) has not been implemented\")\n    }\n}\n"
  },
  {
    "path": "View/ArchiveSettingCell.swift",
    "content": "//\n//  ArchiveSettingCell.swift\n//  Bark\n//\n//  Created by huangfeng on 2020/5/29.\n//  Copyright © 2020 Fin. All rights reserved.\n//\n\nimport UIKit\n\nclass ArchiveSettingCell: BaseTableViewCell<ArchiveSettingCellViewModel> {\n    let switchButton: UISwitch = {\n        let btn = UISwitch()\n        return btn\n    }()\n\n    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {\n        super.init(style: style, reuseIdentifier: reuseIdentifier)\n        self.selectionStyle = .none\n        self.backgroundColor = BKColor.background.secondary\n        self.textLabel?.text = \"defaultArchiveSettings\".localized\n\n        contentView.addSubview(switchButton)\n        switchButton.snp.makeConstraints { make in\n            make.right.equalToSuperview().offset(-16)\n            make.centerY.equalToSuperview()\n        }\n    }\n\n    @available(*, unavailable)\n    required init?(coder aDecoder: NSCoder) {\n        fatalError(\"init(coder:) has not been implemented\")\n    }\n\n    override func bindViewModel(model: ArchiveSettingCellViewModel) {\n        super.bindViewModel(model: model)\n        (self.switchButton.rx.isOn <-> model.on)\n            .disposed(by: rx.reuseBag)\n    }\n}\n"
  },
  {
    "path": "View/ArchiveSettingCellViewModel.swift",
    "content": "//\n//  ArchiveSettingCellViewModel.swift\n//  Bark\n//\n//  Created by huangfeng on 2020/11/20.\n//  Copyright © 2020 Fin. All rights reserved.\n//\n\nimport Foundation\nimport RxCocoa\nclass ArchiveSettingCellViewModel: ViewModel {\n    var on: BehaviorRelay<Bool>\n    init(on: BehaviorRelay<Bool>) {\n        self.on = on\n        super.init()\n    }\n}\n"
  },
  {
    "path": "View/BKButton.swift",
    "content": "//\n//  BKButton.swift\n//  Bark\n//\n//  Created by huangfeng on 2020/9/23.\n//  Copyright © 2020 Fin. All rights reserved.\n//\n\nimport UIKit\n\nprotocol AlignmentRectInsetsOverridable: AnyObject {\n    var alignmentRectInsetsOverride: UIEdgeInsets? { get set }\n}\n\nprotocol HitTestSlopable: AnyObject {\n    var hitTestSlop: UIEdgeInsets { get set }\n}\n\nclass BKButton: UIButton, HitTestSlopable, AlignmentRectInsetsOverridable {\n    var hitTestSlop = UIEdgeInsets.zero\n    override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {\n        if hitTestSlop == UIEdgeInsets.zero {\n            return super.point(inside: point, with: event)\n        }\n        else {\n            return self.bounds.inset(by: hitTestSlop).contains(point)\n        }\n    }\n\n    var alignmentRectInsetsOverride: UIEdgeInsets?\n    override var alignmentRectInsets: UIEdgeInsets {\n        return alignmentRectInsetsOverride ?? super.alignmentRectInsets\n    }\n}\n"
  },
  {
    "path": "View/BKDropDownCell.swift",
    "content": "//\n//  BKDropDownCellTableViewCell.swift\n//  Bark\n//\n//  Created by huangfeng on 2023/2/9.\n//  Copyright © 2023 Fin. All rights reserved.\n//\n\nimport DropDown\nimport UIKit\n\nclass BKDropDownCell: DropDownCell {\n\n    @IBOutlet var selectBackgroundView: UIView!\n\n    override func awakeFromNib() {\n        super.awakeFromNib()\n        self.backgroundColor = BKColor.white\n        self.selectBackgroundView.layer.cornerRadius = 10\n        self.selectBackgroundView.clipsToBounds = true\n    }\n\n    override func setSelected(_ selected: Bool, animated: Bool) {\n\n        let executeSelection: () -> Void = { [weak self] in\n            guard let `self` = self else { return }\n\n            let selectedBackgroundColor = BKColor.grey.lighten5\n            if selected {\n                self.selectBackgroundView.backgroundColor = selectedBackgroundColor\n                self.optionLabel.textColor = BKColor.grey.darken4\n            } else {\n                self.selectBackgroundView.backgroundColor = .clear\n                self.optionLabel.textColor = BKColor.grey.darken3\n            }\n        }\n\n        if animated {\n            UIView.animate(withDuration: 0.3, animations: {\n                executeSelection()\n            })\n        } else {\n            executeSelection()\n        }\n\n        accessibilityTraits = selected ? .selected : .none\n    }\n}\n"
  },
  {
    "path": "View/BKDropDownCell.xib",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<document type=\"com.apple.InterfaceBuilder3.CocoaTouch.XIB\" version=\"3.0\" toolsVersion=\"21507\" targetRuntime=\"iOS.CocoaTouch\" propertyAccessControl=\"none\" useAutolayout=\"YES\" useTraitCollections=\"YES\" useSafeAreas=\"YES\" colorMatched=\"YES\">\n    <device id=\"retina6_12\" orientation=\"portrait\" appearance=\"light\"/>\n    <dependencies>\n        <deployment identifier=\"iOS\"/>\n        <plugIn identifier=\"com.apple.InterfaceBuilder.IBCocoaTouchPlugin\" version=\"21505\"/>\n        <capability name=\"Safe area layout guides\" minToolsVersion=\"9.0\"/>\n        <capability name=\"System colors in document resources\" minToolsVersion=\"11.0\"/>\n        <capability name=\"documents saved in the Xcode 8 format\" minToolsVersion=\"8.0\"/>\n    </dependencies>\n    <objects>\n        <placeholder placeholderIdentifier=\"IBFilesOwner\" id=\"-1\" userLabel=\"File's Owner\"/>\n        <placeholder placeholderIdentifier=\"IBFirstResponder\" id=\"-2\" customClass=\"UIResponder\"/>\n        <tableViewCell clipsSubviews=\"YES\" contentMode=\"scaleToFill\" preservesSuperviewLayoutMargins=\"YES\" selectionStyle=\"default\" indentationWidth=\"10\" rowHeight=\"62\" id=\"Ov8-GI-Rat\" customClass=\"BKDropDownCell\" customModule=\"Bark\" customModuleProvider=\"target\">\n            <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"393\" height=\"62\"/>\n            <autoresizingMask key=\"autoresizingMask\"/>\n            <tableViewCellContentView key=\"contentView\" opaque=\"NO\" clipsSubviews=\"YES\" multipleTouchEnabled=\"YES\" contentMode=\"center\" preservesSuperviewLayoutMargins=\"YES\" insetsLayoutMarginsFromSafeArea=\"NO\" tableViewCell=\"Ov8-GI-Rat\" id=\"JkW-au-90U\">\n                <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"393\" height=\"62\"/>\n                <autoresizingMask key=\"autoresizingMask\"/>\n                <subviews>\n                    <view contentMode=\"scaleToFill\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"Be1-Hg-cfz\" userLabel=\"Select Background View\">\n                        <rect key=\"frame\" x=\"6\" y=\"4\" width=\"381\" height=\"54\"/>\n                        <viewLayoutGuide key=\"safeArea\" id=\"GJ2-4o-aoF\"/>\n                        <color key=\"backgroundColor\" systemColor=\"linkColor\"/>\n                    </view>\n                    <label opaque=\"NO\" userInteractionEnabled=\"NO\" contentMode=\"left\" horizontalHuggingPriority=\"251\" verticalHuggingPriority=\"251\" text=\"Label\" textAlignment=\"natural\" lineBreakMode=\"tailTruncation\" baselineAdjustment=\"alignBaselines\" adjustsFontSizeToFit=\"NO\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"2lm-va-4a1\">\n                        <rect key=\"frame\" x=\"16\" y=\"5\" width=\"361\" height=\"52\"/>\n                        <fontDescription key=\"fontDescription\" type=\"system\" pointSize=\"17\"/>\n                        <nil key=\"textColor\"/>\n                        <nil key=\"highlightedColor\"/>\n                    </label>\n                </subviews>\n                <constraints>\n                    <constraint firstAttribute=\"bottom\" secondItem=\"2lm-va-4a1\" secondAttribute=\"bottom\" constant=\"5\" id=\"A61-Or-MrS\"/>\n                    <constraint firstItem=\"Be1-Hg-cfz\" firstAttribute=\"leading\" secondItem=\"JkW-au-90U\" secondAttribute=\"leading\" constant=\"6\" id=\"Cl3-sY-bfR\"/>\n                    <constraint firstAttribute=\"trailing\" secondItem=\"Be1-Hg-cfz\" secondAttribute=\"trailing\" constant=\"6\" id=\"M5z-T7-Vbz\"/>\n                    <constraint firstItem=\"Be1-Hg-cfz\" firstAttribute=\"top\" secondItem=\"JkW-au-90U\" secondAttribute=\"top\" constant=\"4\" id=\"YzQ-6y-cJa\"/>\n                    <constraint firstItem=\"2lm-va-4a1\" firstAttribute=\"top\" secondItem=\"JkW-au-90U\" secondAttribute=\"top\" constant=\"5\" id=\"cix-Ub-eyW\"/>\n                    <constraint firstAttribute=\"trailing\" secondItem=\"2lm-va-4a1\" secondAttribute=\"trailing\" constant=\"16\" id=\"fmp-HQ-Icr\"/>\n                    <constraint firstItem=\"2lm-va-4a1\" firstAttribute=\"leading\" secondItem=\"JkW-au-90U\" secondAttribute=\"leading\" constant=\"16\" id=\"kdu-uI-TlX\"/>\n                    <constraint firstAttribute=\"bottom\" secondItem=\"Be1-Hg-cfz\" secondAttribute=\"bottom\" constant=\"4\" id=\"twp-ah-AEA\"/>\n                </constraints>\n            </tableViewCellContentView>\n            <connections>\n                <outlet property=\"optionLabel\" destination=\"2lm-va-4a1\" id=\"try-1f-LgE\"/>\n                <outlet property=\"selectBackgroundView\" destination=\"Be1-Hg-cfz\" id=\"mpw-MN-DAU\"/>\n            </connections>\n            <point key=\"canvasLocation\" x=\"46.564885496183201\" y=\"-64.08450704225352\"/>\n        </tableViewCell>\n    </objects>\n    <resources>\n        <systemColor name=\"linkColor\">\n            <color red=\"0.0\" green=\"0.47843137254901963\" blue=\"1\" alpha=\"1\" colorSpace=\"custom\" customColorSpace=\"sRGB\"/>\n        </systemColor>\n    </resources>\n</document>\n"
  },
  {
    "path": "View/BKLabel.swift",
    "content": "//\n//  BKLabel.swift\n//  Bark\n//\n//  Created by huangfeng on 2020/5/29.\n//  Copyright © 2020 Fin. All rights reserved.\n//\n\nimport UIKit\n\nclass BKLabel: UILabel {\n    var hitTestSlop = UIEdgeInsets.zero\n\n    override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {\n        if hitTestSlop == UIEdgeInsets.zero {\n            return super.point(inside: point, with: event)\n        }\n        else {\n            return self.bounds.inset(by: hitTestSlop).contains(point)\n        }\n    }\n}\n"
  },
  {
    "path": "View/BaseTableViewCell.swift",
    "content": "//\n//  BaseTableViewCell.swift\n//  Bark\n//\n//  Created by huangfeng on 2020/11/20.\n//  Copyright © 2020 Fin. All rights reserved.\n//\n\nimport UIKit\n\nclass BaseTableViewCell<T>: UITableViewCell where T: ViewModel {\n    var viewModel: T?\n    func bindViewModel(model: T) {\n        self.viewModel = model\n    }\n}\n"
  },
  {
    "path": "View/BorderTextField.swift",
    "content": "//\n//  BorderTextField.swift\n//  Bark\n//\n//  Created by huangfeng on 2023/2/6.\n//  Copyright © 2023 Fin. All rights reserved.\n//\n\nimport UIKit\n\nclass InsetTextField: UITextField {\n    var insets = UIEdgeInsets.zero\n\n    override func textRect(forBounds bounds: CGRect) -> CGRect {\n        let bounds = super.textRect(forBounds: bounds)\n        return bounds.inset(by: insets)\n    }\n\n    override func editingRect(forBounds bounds: CGRect) -> CGRect {\n        let bounds = super.textRect(forBounds: bounds)\n        return bounds.inset(by: insets)\n    }\n\n    override func placeholderRect(forBounds bounds: CGRect) -> CGRect {\n        let bounds = super.textRect(forBounds: bounds)\n        return bounds.inset(by: insets)\n    }\f\n}\n\nclass BorderTextField: InsetTextField {\n    var isSelecting: Bool = true {\n        didSet {\n            UIView.animate(withDuration: 0.3) {\n                if self.isSelecting {\n                    self.backgroundView.borderColor = BKColor.blue.darken5\n                    self.backgroundView.shadowColor = BKColor.blue.darken5\n                    self.backgroundView.layer.shadowOpacity = 0.3\n                }\n                else {\n                    self.backgroundView.borderColor = BKColor.grey.lighten2\n                    self.backgroundView.shadowColor = BKColor.grey.lighten2\n                    self.backgroundView.layer.shadowOpacity = 0\n                }\n            }\n        }\n    }\n    \n    let backgroundView: UIView = {\n        let view = UIView()\n        view.backgroundColor = BKColor.white\n        view.isUserInteractionEnabled = false\n        view.cornerRadiusPreset = .cornerRadius3\n        view.shadowColor = BKColor.grey.lighten2\n        view.layer.shadowOffset = CGSize(width: 0, height: 0)\n        view.layer.shadowRadius = 2\n        view.layer.shadowOpacity = 0\n        view.borderColor = BKColor.grey.lighten2\n        view.borderWidthPreset = .border2\n        \n        return view\n    }()\n    \n    init(title: String? = nil) {\n        super.init(frame: CGRect.zero)\n        self.insets = UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 16)\n\n        self.textColor = BKColor.grey.darken3\n        self.font = UIFont.preferredFont(ofSize: 14)\n        self.adjustsFontForContentSizeCategory = true\n        self.textAlignment = .left\n        \n        self.insertSubview(backgroundView, at: 0)\n        backgroundView.snp.makeConstraints { make in\n            make.edges.equalToSuperview()\n        }\n        \n        self.delegate = self\n    }\n    override var placeholder: String? {\n        didSet{\n            self.attributedPlaceholder = NSAttributedString(string: placeholder ?? \"\" , attributes: [\n                .font: self.font ?? UIFont.preferredFont(ofSize: 14),\n                .foregroundColor: BKColor.grey.darken1\n            ])\n        }\n    }\n    \n\n    @available(*, unavailable)\n    required init?(coder: NSCoder) {\n        fatalError(\"init(coder:) has not been implemented\")\n    }\n}\n\nextension BorderTextField: UITextFieldDelegate {\n    func textFieldDidBeginEditing(_ textField: UITextField) {\n        self.isSelecting = true\n    }\n    func textFieldDidEndEditing(_ textField: UITextField) {\n        self.isSelecting = false\n    }\n}\n"
  },
  {
    "path": "View/DonateCell.swift",
    "content": "//\n//  DonateCell.swift\n//  Bark\n//\n//  Created by huangfeng on 11/13/24.\n//  Copyright © 2024 Fin. All rights reserved.\n//\n\nimport SwiftyStoreKit\nimport UIKit\n\nclass DonateCell: UITableViewCell {\n    var title: String? = nil {\n        didSet {\n            self.textLabel?.text = title\n        }\n    }\n\n    var productId: String? = nil {\n        didSet {\n            guard let productId else { return }\n            if let cachePriceStr = Settings[\"bark.price.\\(productId)\"] {\n                self.detailTextLabel?.text = cachePriceStr\n                return\n            }\n            // 查询价格\n            SwiftyStoreKit.retrieveProductsInfo([productId]) { result in\n                if let product = result.retrievedProducts.first, let price = product.localizedPrice {\n                    let priceStr = price + (product.localizedSubscriptionPeriod.isEmpty ? \"\" : \" / \\(product.localizedSubscriptionPeriod)\")\n                    Settings[\"bark.price.\\(productId)\"] = priceStr\n                    self.detailTextLabel?.text = priceStr\n                }\n            }\n        }\n    }\n\n    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {\n        super.init(style: .value1, reuseIdentifier: reuseIdentifier)\n        self.selectionStyle = .none\n        self.accessoryType = .disclosureIndicator\n        self.backgroundColor = BKColor.background.secondary\n        self.detailTextLabel?.textColor = BKColor.grey.darken2\n    }\n    \n    @available(*, unavailable)\n    required init?(coder aDecoder: NSCoder) {\n        fatalError(\"init(coder:) has not been implemented\")\n    }\n}\n"
  },
  {
    "path": "View/DropBoxView.swift",
    "content": "//\n//  DropBoxView.swift\n//  Bark\n//\n//  Created by huangfeng on 2023/2/2.\n//  Copyright © 2023 Fin. All rights reserved.\n//\n\nimport DropDown\nimport RxCocoa\nimport RxSwift\nimport UIKit\n\nclass DropBoxView: UIView {\n    let valueLabel: UILabel = {\n        let label = UILabel()\n        label.font = UIFont.preferredFont(ofSize: 14)\n        label.adjustsFontForContentSizeCategory = true\n        label.textColor = BKColor.grey.darken3\n        return label\n    }()\n\n    let dropIconView: UIImageView = {\n        let imageView = UIImageView()\n        imageView.image = UIImage(named: \"baseline_keyboard_arrow_down_black_24pt\")?.withRenderingMode(.alwaysTemplate)\n        imageView.tintColor = BKColor.grey.lighten2\n        return imageView\n    }()\n\n    var isSelecting: Bool = true {\n        didSet {\n            UIView.animate(withDuration: 0.3) {\n                if self.isSelecting {\n                    self.borderColor = BKColor.blue.darken5\n                    self.shadowColor = BKColor.blue.darken5\n                    self.layer.shadowOpacity = 0.3\n                } else {\n                    self.borderColor = BKColor.grey.lighten2\n                    self.shadowColor = BKColor.grey.lighten2\n                    self.layer.shadowOpacity = 0\n                }\n            }\n        }\n    }\n\n    var values: [String] {\n        didSet {\n            self.currentValue = values.first\n        }\n    }\n\n    var currentValue: String? {\n        didSet {\n            self.valueLabel.text = currentValue\n            self.currentValueSubject.onNext(self.currentValue)\n        }\n    }\n\n    fileprivate let currentValueSubject = PublishSubject<String?>()\n\n    init(values: [String]) {\n        self.values = values\n        super.init(frame: CGRect.zero)\n        self.backgroundColor = BKColor.white\n\n        self.borderColor = BKColor.grey.lighten2\n        self.borderWidthPreset = .border2\n        self.cornerRadiusPreset = .cornerRadius3\n        self.shadowColor = BKColor.grey.lighten2\n        self.layer.shadowOffset = CGSize(width: 0, height: 0)\n        self.layer.shadowRadius = 2\n        self.layer.shadowOpacity = 0\n\n        addSubview(valueLabel)\n        addSubview(dropIconView)\n\n        self.dropIconView.snp.makeConstraints { make in\n            make.centerY.equalToSuperview()\n            make.right.equalTo(-6)\n        }\n        self.valueLabel.snp.makeConstraints { make in\n            make.centerY.equalToSuperview()\n            make.left.equalTo(16)\n        }\n\n        self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tap)))\n\n        defer {\n            self.currentValue = self.values.first\n        }\n    }\n\n    @available(*, unavailable)\n    required init?(coder: NSCoder) {\n        fatalError(\"init(coder:) has not been implemented\")\n    }\n\n    @objc func tap() {\n        let dropDown = DropDown(anchorView: self)\n        dropDown.cellNib = UINib(nibName: \"BKDropDownCell\", bundle: Bundle(for: BKDropDownCell.self))\n        dropDown.cellHeight = 50\n        dropDown.cornerRadius = 10\n        dropDown.clipsToBounds = true\n        dropDown.bottomOffset = CGPoint(x: 0, y: 50)\n\n        dropDown.dataSource = self.values\n\n        dropDown.selectionAction = { [weak self] _, str in\n            self?.currentValue = str\n            self?.isSelecting = false\n        }\n        dropDown.cancelAction = { [weak self] in\n            self?.isSelecting = false\n        }\n        self.isSelecting = true\n        dropDown.show()\n    }\n}\n\nextension Reactive where Base: DropBoxView {\n    var currentValueChanged: ControlEvent<String?> {\n        let source = base.currentValueSubject.asObservable()\n        return ControlEvent(events: source)\n    }\n}\n"
  },
  {
    "path": "View/GesturePassTextView.swift",
    "content": "//\n//  GesturePassTextView.swift\n//  Bark\n//\n//  Created by huangfeng on 2023/7/26.\n//  Copyright © 2023 Fin. All rights reserved.\n//\n\nimport UIKit\n\nclass GesturePassTextView: UITextView {\n    var superCell: UITableViewCell? = nil\n    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {\n        if let cell = superCell {\n            cell.touchesBegan(touches, with: event)\n            return\n        }\n        super.touchesBegan(touches, with: event)\n    }\n    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {\n        if let cell = superCell {\n            cell.touchesMoved(touches, with: event)\n            return\n        }\n        super.touchesMoved(touches, with: event)\n    }\n    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {\n        if let cell = superCell {\n            cell.touchesEnded(touches, with: event)\n            return\n        }\n        super.touchesEnded(touches, with: event)\n    }\n    override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {\n        if let cell = superCell {\n            cell.touchesCancelled(touches, with: event)\n            return\n        }\n        super.touchesCancelled(touches, with: event)\n    }\n}\n"
  },
  {
    "path": "View/GradientButton.swift",
    "content": "//\n//  UIView+Gradient.swift\n//  Bark\n//\n//  Created by huangfeng on 2023/2/10.\n//  Copyright © 2023 Fin. All rights reserved.\n//\n\nimport UIKit\n\ntypealias GradientPoints = (startPoint: CGPoint, endPoint: CGPoint)\n\nenum GradientOrientation {\n    case topRightBottomLeft\n    case topLeftBottomRight\n    case horizontal\n    case vertical\n\n    var startPoint: CGPoint {\n        return points.startPoint\n    }\n\n    var endPoint: CGPoint {\n        return points.endPoint\n    }\n\n    var points: GradientPoints {\n        switch self {\n        case .topRightBottomLeft:\n            return (CGPoint(x: 0.0, y: 1.0), CGPoint(x: 1.0, y: 0.0))\n        case .topLeftBottomRight:\n            return (CGPoint(x: 0.0, y: 0.0), CGPoint(x: 1, y: 1))\n        case .horizontal:\n            return (CGPoint(x: 0.0, y: 0.5), CGPoint(x: 1.0, y: 0.5))\n        case .vertical:\n            return (CGPoint(x: 0.0, y: 0.0), CGPoint(x: 0.0, y: 1.0))\n        }\n    }\n}\n\nclass GradientButton: UIButton {\n    lazy var gradient: CAGradientLayer = {\n        let gradient = CAGradientLayer()\n        return gradient\n    }()\n\n    func applyGradient(withColours colours: [UIColor], gradientOrientation orientation: GradientOrientation) {\n        gradient.frame = self.bounds\n        gradient.colors = colours.map { $0.cgColor }\n        gradient.startPoint = orientation.startPoint\n        gradient.endPoint = orientation.endPoint\n\n        if gradient.superlayer == nil {\n            self.layer.insertSublayer(gradient, at: 0)\n        }\n    }\n\n    override func layoutSubviews() {\n        super.layoutSubviews()\n        gradient.frame = self.bounds\n    }\n}\n"
  },
  {
    "path": "View/HUD.swift",
    "content": "//\n//  HUD.swift\n//  Bark\n//\n//  Created by huangfeng on 2023/3/6.\n//  Copyright © 2023 Fin. All rights reserved.\n//\n\nimport SVProgressHUD\nimport UIKit\nclass BarkProgressHUD: SVProgressHUD {\n    override class func displayDuration(for string: String?) -> TimeInterval {\n        return min(Double((string ?? \"\").utf8.count) * 0.06 + 0.5, 5.0)\n    }\n}\n\nopen class ProgressHUD: NSObject {\n    open class func show() {\n        BarkProgressHUD.show()\n    }\n\n    open class func showWithClearMask() {\n        BarkProgressHUD.show()\n    }\n\n    open class func dismiss() {\n        BarkProgressHUD.dismiss()\n    }\n\n    open class func showWithStatus(_ status: String!) {\n        BarkProgressHUD.show(withStatus: status)\n    }\n\n    open class func success(_ status: String!) {\n        BarkProgressHUD.showSuccess(withStatus: status)\n    }\n\n    open class func error(_ status: String!) {\n        BarkProgressHUD.showError(withStatus: status)\n    }\n\n    open class func inform(_ status: String!) {\n        BarkProgressHUD.showInfo(withStatus: status)\n    }\n}\n\npublic func HUDSuccess(_ status: String?) {\n    ProgressHUD.success(status ?? \"\")\n}\n\npublic func HUDError(_ status: String?) {\n    ProgressHUD.error(status ?? \"\")\n}\n\npublic func HUDInform(_ status: String?) {\n    ProgressHUD.inform(status ?? \"\")\n}\n\npublic func HUDShow() {\n    ProgressHUD.show()\n}\n\npublic func HUDShowWithStatus(_ status: String!) {\n    ProgressHUD.showWithStatus(status)\n}\n\npublic func HUDDismiss() {\n    ProgressHUD.dismiss()\n}\n"
  },
  {
    "path": "View/InsetView.swift",
    "content": "//\n//  InsetView.swift\n//  Bark\n//\n//  Created by huangfeng on 9/8/25.\n//  Copyright © 2025 Fin. All rights reserved.\n//\n\nimport UIKit\n\nclass InsetView: UIView {\n    init(subView: UIView, insets: UIEdgeInsets) {\n        super.init(frame: .zero)\n        self.addSubview(subView)\n        subView.snp.makeConstraints { make in\n            make.edges.equalToSuperview().inset(insets)\n        }\n    }\n    @available(*, unavailable)\n    required init?(coder: NSCoder) {\n        fatalError(\"init(coder:) has not been implemented\")\n    }\n}\n"
  },
  {
    "path": "View/LabelCell.swift",
    "content": "//\n//  LabelCell.swift\n//  Bark\n//\n//  Created by huangfeng on 2020/5/29.\n//  Copyright © 2020 Fin. All rights reserved.\n//\n\nimport Material\nimport UIKit\n\nclass LabelCell: UITableViewCell {\n    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {\n        super.init(style: style, reuseIdentifier: reuseIdentifier)\n        self.selectionStyle = .none\n\n        self.backgroundColor = BKColor.background.primary\n        self.textLabel?.textColor = BKColor.grey.darken1\n        self.textLabel?.fontSize = 12\n        self.textLabel?.numberOfLines = 0\n    }\n\n    @available(*, unavailable)\n    required init?(coder aDecoder: NSCoder) {\n        fatalError(\"init(coder:) has not been implemented\")\n    }\n}\n"
  },
  {
    "path": "View/MessageList/CustomTapTextView.swift",
    "content": "//\n//  CustomTapTextView.swift\n//  Bark\n//\n//  Created by huangfeng on 12/30/24.\n//  Copyright © 2024 Fin. All rights reserved.\n//\n\nimport UIKit\n\n/// 可以自定义点击事件的 UITextView，同时保留 UITextView 的所有其他手势\n/// 此 TextView  不可编辑， 不可滚动\nclass CustomTapTextView: UITextView, UIGestureRecognizerDelegate {\n    /// 点击手势，如果有选中文字，则不触发\n    private lazy var tapGesture = UITapGestureRecognizer(target: self, action: #selector(tap))\n    /// 双击手势，只是为了让 tapGesture 不要在双击选中文本时触发，没有其他作用\n    private let doubleTapGesture = UITapGestureRecognizer()\n    /// UITextView 自带的点击链接手势\n    private var linkTapGesture: UIGestureRecognizer? = nil\n    \n    /// 额外的单击事件\n    var customTapAction: (() -> Void)?\n    \n    init() {\n        super.init(frame: .zero, textContainer: nil)\n        \n        self.backgroundColor = UIColor.clear\n        self.isEditable = false\n        self.dataDetectorTypes = [.phoneNumber, .link]\n        self.isScrollEnabled = false\n        self.textContainerInset = .zero\n        self.textContainer.lineFragmentPadding = 0\n        \n        tapGesture.delegate = self\n        self.addGestureRecognizer(tapGesture)\n        \n        self.linkTapGesture = self.gestureRecognizers?.first { $0 is UITapGestureRecognizer && $0.name == \"UITextInteractionNameLinkTap\" }\n        \n        doubleTapGesture.numberOfTapsRequired = 2\n        doubleTapGesture.delegate = self\n        self.addGestureRecognizer(doubleTapGesture)\n    }\n\n    @available(*, unavailable)\n    required init?(coder: NSCoder) {\n        fatalError(\"init(coder:) has not been implemented\")\n    }\n\n    @objc func tap() {\n        self.customTapAction?()\n    }\n    \n    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {\n        if gestureRecognizer == doubleTapGesture {\n            return true\n        }\n        return false\n    }\n\n    override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {\n        if gestureRecognizer == tapGesture {\n            if self.selectedRange.length > 0 {\n                return false\n            }\n        }\n        return super.gestureRecognizerShouldBegin(gestureRecognizer)\n    }\n    \n    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool {\n        if gestureRecognizer == tapGesture {\n            if otherGestureRecognizer == doubleTapGesture {\n                return true\n            }\n            if otherGestureRecognizer == linkTapGesture {\n                return true\n            }\n        }\n        return false\n    }\n}\n"
  },
  {
    "path": "View/MessageList/MessageGroupHeaderView.swift",
    "content": "//\n//  MessageGroupHeaderView.swift\n//  Bark\n//\n//  Created by huangfeng on 12/23/24.\n//  Copyright © 2024 Fin. All rights reserved.\n//\n\nimport UIKit\n\nclass MessageGroupHeaderView: UIView {\n    private let groupNameLabel: UILabel = {\n        let label = UILabel()\n        label.font = UIFont.preferredFont(ofSize: 16, weight: .semibold)\n        label.textColor = BKColor.grey.darken4\n        return label\n    }()\n\n    private let showLessAndClearView: ShowLessAndClearView = {\n        let view = ShowLessAndClearView()\n        return view\n    }()\n    \n    var groupName: String? {\n        didSet {\n            groupNameLabel.text = groupName\n        }\n    }\n\n    var showLessAction: (() -> Void)? {\n        didSet {\n            showLessAndClearView.showLessAction = showLessAction\n        }\n    }\n    \n    var clearAction: (() -> Void)? {\n        didSet {\n            showLessAndClearView.clearAction = clearAction\n        }\n    }\n    \n    init() {\n        super.init(frame: .zero)\n        addSubview(groupNameLabel)\n        addSubview(showLessAndClearView)\n        \n        groupNameLabel.snp.makeConstraints { make in\n            make.left.equalTo(8)\n            make.top.equalTo(10)\n            make.bottom.equalTo(-10)\n        }\n        showLessAndClearView.snp.makeConstraints { make in\n            make.right.equalToSuperview()\n            make.centerY.equalTo(groupNameLabel)\n        }\n        \n        self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tap)))\n    }\n\n    @available(*, unavailable)\n    required init?(coder: NSCoder) {\n        fatalError(\"init(coder:) has not been implemented\")\n    }\n    \n    @objc func tap() {\n        showLessAction?()\n    }\n}\n"
  },
  {
    "path": "View/MessageList/MessageGroupMoreView.swift",
    "content": "//\n//  MessageGroupMoreView.swift\n//  Bark\n//\n//  Created by huangfeng on 12/25/24.\n//  Copyright © 2024 Fin. All rights reserved.\n//\n\nimport UIKit\n\nclass MessageGroupMoreView: UIView {\n    private let moreLabel: UILabel = {\n        let label = UILabel()\n        label.textColor = BKColor.grey.darken3\n        label.font = UIFont.preferredFont(ofSize: 12)\n        return label\n    }()\n    \n    let arrowImageView: UIImageView = {\n        let imageView = UIImageView()\n        imageView.image = UIImage(named: \"keyboard_arrow_right_symbol\")?.withRenderingMode(.alwaysTemplate)\n        imageView.tintColor = BKColor.grey.darken2\n        return imageView\n    }()\n    \n    var count: Int = 0 {\n        didSet {\n            moreLabel.text = \"viewAllMessages\".localized(with: count)\n        }\n    }\n    \n    init() {\n        super.init(frame: CGRect.zero)\n        self.backgroundColor = BKColor.grey.lighten5\n        self.layer.cornerRadius = 28 / 2\n        self.clipsToBounds = true\n        \n        self.addSubview(moreLabel)\n        self.addSubview(arrowImageView)\n        moreLabel.snp.makeConstraints { make in\n            make.left.equalToSuperview().offset(8)\n            make.height.equalTo(28).priority(.medium)\n            make.top.bottom.equalToSuperview()\n        }\n        arrowImageView.snp.makeConstraints { make in\n            make.right.equalToSuperview().offset(-6)\n            make.centerY.equalToSuperview()\n            make.left.equalTo(moreLabel.snp.right).offset(4)\n        }\n    }\n\n    @available(*, unavailable)\n    required init?(coder: NSCoder) {\n        fatalError(\"init(coder:) has not been implemented\")\n    }\n}\n"
  },
  {
    "path": "View/MessageList/MessageItemView.swift",
    "content": "//\n//  MessageItemView.swift\n//  Bark\n//\n//  Created by huangfeng on 12/23/24.\n//  Copyright © 2024 Fin. All rights reserved.\n//\n\nimport ImageViewer_swift\nimport Kingfisher\nimport Photos\nimport SVProgressHUD\nimport UIKit\n\nclass MessageItemView: UIView {\n    let panel: UIView = {\n        let view = UIView()\n        view.layer.cornerRadius = 10\n        view.backgroundColor = BKColor.background.secondary\n        view.clipsToBounds = true\n        return view\n    }()\n    \n    let blackMaskView: UIView = {\n        let view = UIView()\n        view.layer.cornerRadius = 10\n        view.backgroundColor = BKColor.black\n        view.isUserInteractionEnabled = false\n        view.alpha = 0\n        return view\n    }()\n    \n    let bodyLabel: CustomTapTextView = {\n        let label = CustomTapTextView()\n        label.font = UIFont.preferredFont(ofSize: 14)\n        label.adjustsFontForContentSizeCategory = true\n        label.textColor = BKColor.grey.darken4\n        return label\n    }()\n    \n    let imageView: UIImageView = {\n        let imageView = UIImageView()\n        imageView.contentMode = .scaleAspectFit\n        imageView.layer.cornerRadius = 4\n        imageView.clipsToBounds = true\n        return imageView\n    }()\n    \n    let contentStackView: UIStackView = {\n        let stackView = UIStackView()\n        stackView.axis = .vertical\n        stackView.spacing = 8\n        stackView.alignment = .leading\n        return stackView\n    }()\n    \n    let dateLabel: UILabel = {\n        let label = BKLabel()\n        label.hitTestSlop = UIEdgeInsets(top: -5, left: -5, bottom: -5, right: -5)\n        label.font = UIFont.preferredFont(ofSize: 11, weight: .medium)\n        label.adjustsFontForContentSizeCategory = true\n        label.textColor = BKColor.grey.base\n        label.isUserInteractionEnabled = true\n        label.addGestureRecognizer(UITapGestureRecognizer())\n        return label\n    }()\n\n    var message: MessageItemModel? = nil {\n        didSet {\n            guard let message else {\n                return\n            }\n            setMessage(message: message)\n        }\n    }\n\n    var maskAlpha: CGFloat = 0 {\n        didSet {\n            blackMaskView.alpha = maskAlpha\n        }\n    }\n    \n    var isShowSubviews: Bool = true {\n        didSet {\n            for view in [bodyLabel, dateLabel] {\n                view.alpha = isShowSubviews ? 1 : 0\n            }\n        }\n    }\n    \n    var tapAction: ((_ message: MessageItemModel, _ sourceView: MessageItemView) -> Void)?\n    \n    static var imageCache: ImageCache?\n    static var imageCacheCreatedTime: Date?\n    /// 用于查找通知扩展缓存的图片\n    \n    init() {\n        super.init(frame: .zero)\n        self.backgroundColor = BKColor.background.primary\n        self.addSubview(panel)\n        panel.addSubview(contentStackView)\n        panel.addSubview(dateLabel)\n        panel.addSubview(blackMaskView)\n        contentStackView.addArrangedSubview(bodyLabel)\n        contentStackView.addArrangedSubview(imageView)\n        \n        self.isAccessibilityElement = true\n        self.subviews.forEach { $0.isAccessibilityElement = false }\n\n        layoutView()\n        \n        // 切换时间显示样式\n        dateLabel.gestureRecognizers?.first?.rx.event.subscribe(onNext: { [weak self] _ in\n            guard let self else { return }\n            if self.message?.dateStyle != .exact {\n                self.message?.dateStyle = .exact\n            } else {\n                self.message?.dateStyle = .relative\n            }\n            self.dateLabel.text = self.message?.dateText\n        }).disposed(by: rx.disposeBag)\n        \n        self.bodyLabel.customTapAction = { [weak self] in\n            guard let self, let message = self.message else { return }\n            self.tapAction?(message, self)\n        }\n        \n        panel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tap)))\n    }\n\n    @available(*, unavailable)\n    required init?(coder: NSCoder) {\n        fatalError(\"init(coder:) has not been implemented\")\n    }\n    \n    func layoutView() {\n        contentStackView.snp.makeConstraints { make in\n            make.top.equalTo(16)\n            make.left.equalTo(12)\n            make.right.equalTo(-12)\n        }\n        dateLabel.snp.makeConstraints { make in\n            make.left.equalTo(contentStackView)\n            make.top.equalTo(contentStackView.snp.bottom).offset(12)\n            make.bottom.equalTo(panel).offset(-12).priority(.medium)\n        }\n        panel.snp.makeConstraints { make in\n            make.left.equalToSuperview().offset(16)\n            make.right.equalToSuperview().offset(-16)\n            make.top.equalToSuperview()\n            make.bottom.equalToSuperview()\n        }\n        blackMaskView.snp.makeConstraints { make in\n            make.edges.equalTo(panel)\n        }\n    }\n    \n    @objc func tap() {\n        guard let message else { return }\n        self.tapAction?(message, self)\n    }\n}\n\nextension MessageItemView {\n    /// 获取图片缓存，用于查找由通知扩展缓存的图片\n    /// - Note: 如果创建的图片缓存时间点在传入 date 之前，则需要重新创建图片缓存。因为 Kingfisher/DiskStorage 会在创建时，使用 maybeCached Set 缓存图片路径。\n    /// 由于 Bark 会在 NotificationServiceExtension 使用新的 ImageCache 示例缓存图片， 导致新缓存的图片没有更新到主 APP 的 ImageCache 实例中的 maybeCached，于是被误认为没有缓存导致问题\n    /// - Parameter date: 图片缓存有效时间点\n    /// - Returns: 图片缓存\n    func getImageCache(date: Date) -> ImageCache {\n        if let cache = MessageItemView.imageCache, let createdTime = MessageItemView.imageCacheCreatedTime, createdTime > date {\n            return cache\n        }\n        let groupUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: \"group.bark\")\n        let cache = try? ImageCache(name: \"shared\", cacheDirectoryURL: groupUrl)\n        MessageItemView.imageCache = cache\n        MessageItemView.imageCacheCreatedTime = Date()\n        return cache ?? ImageCache.default\n    }\n\n    func setMessage(message: MessageItemModel) {\n        self.bodyLabel.attributedText = message.attributedText\n        self.accessibilityLabel = message.attributedText?.string\n        self.dateLabel.text = message.dateText\n        if let image = message.image {\n            imageView.isHidden = false\n            // 图片未缓存时，使用的默认尺寸\n            remakeImageViewConstraints(width: 200, height: 100)\n            // 移除图片查看器\n            imageView.removeImageViewer()\n            \n            // loadDiskFileSynchronously\n            imageView.kf.setImage(with: URL(string: image), options: [.targetCache(getImageCache(date: message.createDate ?? Date())), .keepCurrentImageWhileLoading, .loadDiskFileSynchronously]) { [weak self] result in\n                guard let self else { return }\n                guard let image = try? result.get().image else {\n                    self.imageView.image = nil\n                    return\n                }\n                \n                // 获取系统是否是夜间模式\n                let isDarkMode = UIScreen.main.traitCollection.userInterfaceStyle == .dark\n                var options: [ImageViewerOption] = [\n                    .closeIcon(UIImage(named: \"back\")!),\n                    .theme(isDarkMode ? .dark : .light),\n                    .contentMode(.scaleAspectFit)\n                ]\n                if #available(iOS 14.0, *) {\n                    options.append(.rightNavItemTitle(\"save\".localized, onTap: { [weak self] _ in\n                        // 保存 image 到相册\n                        self?.saveImageToAlbum(image)\n                    }))\n                }\n                self.imageView.setupImageViewer(options: options)\n                \n                layoutImageView(image: image)\n            }\n        } else {\n            imageView.isHidden = true\n            remakeImageViewConstraints(width: 0, height: 0)\n        }\n    }\n    \n    func layoutImageView(image: UIImage) {\n        let scale = image.size.height / image.size.width\n        // iPad 下，图片宽度不超过 500。如果图片尺寸小于控件宽度，则以实际图片尺寸作为宽度\n        var width = min(min(500, UIScreen.main.bounds.width - 32 - 24), image.width)\n        var height = width * scale\n        \n        if height > 400 {\n            width = 400 / scale\n            height = 400\n        }\n        \n        remakeImageViewConstraints(width: width, height: height)\n    }\n    \n    func remakeImageViewConstraints(width: CGFloat, height: CGFloat) {\n        imageView.snp.remakeConstraints { make in\n            make.width.equalTo(width)\n            make.height.equalTo(height)\n        }\n    }\n}\n\n@available(iOS 14.0, *)\nextension MessageItemView {\n    func saveImageToAlbum(_ image: UIImage) {\n        PHPhotoLibrary.requestAuthorization(for: .addOnly) { status in\n            guard status == .authorized || status == .limited else {\n                DispatchQueue.main.async {\n                    SVProgressHUD.showInfo(withStatus: \"noPermission\".localized)\n                }\n                return\n            }\n            PHPhotoLibrary.shared().performChanges({\n                PHAssetChangeRequest.creationRequestForAsset(from: image)\n            }) { success, error in\n                DispatchQueue.main.async {\n                    if success {\n                        SVProgressHUD.showSuccess(withStatus: \"saveSuccess\".localized)\n                    } else {\n                        SVProgressHUD.showError(withStatus: error?.localizedDescription)\n                    }\n                }\n            }\n        }\n    }\n}\n\nextension UIImageView {\n    func removeImageViewer() {\n        var _tapRecognizer: UIGestureRecognizer?\n        gestureRecognizers?.forEach {\n            // 手势类名是 TapWithDataRecognizer\n            if \"\\(type(of: $0))\" == \"TapWithDataRecognizer\" {\n                _tapRecognizer = $0\n            }\n        }\n        if let _tapRecognizer {\n            self.removeGestureRecognizer(_tapRecognizer)\n        }\n    }\n}\n"
  },
  {
    "path": "View/MessageList/MessageTableViewCell.swift",
    "content": "//\n//  MessageTableViewCell.swift\n//  Bark\n//\n//  Created by huangfeng on 2020/5/26.\n//  Copyright © 2020 Fin. All rights reserved.\n//\n\nimport Material\nimport RxSwift\nimport SnapKit\nimport UIKit\n\n/// 单个消息 cell\nclass MessageTableViewCell: UITableViewCell {\n    private let messageView = MessageItemView()\n    var message: MessageItemModel? {\n        get {\n            return messageView.message\n        }\n        set {\n            messageView.message = newValue\n        }\n    }\n\n    var tapAction: ((_ message: MessageItemModel, _ sourceView: MessageItemView) -> Void)? {\n        didSet {\n            messageView.tapAction = tapAction\n        }\n    }\n\n    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {\n        super.init(style: style, reuseIdentifier: reuseIdentifier)\n        self.selectionStyle = .none\n        self.contentView.backgroundColor = BKColor.background.primary\n        self.contentView.addSubview(messageView)\n        messageView.snp.makeConstraints { make in\n            make.left.top.right.equalToSuperview()\n            make.bottom.equalTo(-10)\n        }\n    }\n    \n    @available(*, unavailable)\n    required init?(coder: NSCoder) {\n        fatalError(\"init(coder:) has not been implemented\")\n    }\n}\n\n/// 群组  cell\nclass MessageGroupTableViewCell: UITableViewCell {\n    /// 方便做动画，实际没什么用\n    private let panel: UIView = {\n        let panel = UIView()\n        panel.backgroundColor = BKColor.background.primary\n        return panel\n    }()\n\n    /// 消息列表，最多显示 5 条，如果不足5条，多余的会隐藏\n    private let messageViews = [\n        MessageItemView(),\n        MessageItemView(),\n        MessageItemView(),\n        MessageItemView(),\n        MessageItemView()\n    ]\n    \n    /// 群组 header ，包含标题、折叠按钮、清除按钮\n    private let groupHeader = MessageGroupHeaderView()\n    /// 查看更多按钮，消息数小于等于 5 时会隐藏\n    private let moreView = MessageGroupMoreView()\n    /// 群组 header top offset 约束\n    private var groupHeaderTopConstraint: Constraint? = nil\n\n    /// 是否展开\n    var isExpanded: Bool = false {\n        didSet {\n            refreshViewState()\n            refreshLayout()\n            self.contentView.layoutIfNeeded()\n        }\n    }\n    \n    /// 消息列表\n    private var messages: [MessageItemModel] = [] {\n        didSet {\n            for (index, item) in messageViews.enumerated() {\n                if index < messages.count {\n                    item.message = messages[index]\n                    item.isHidden = false\n                } else {\n                    item.isHidden = true\n                }\n            }\n        }\n    }\n\n    /// 剩余消息数量\n    private var totalCount: Int = 0 {\n        didSet {\n            moreView.count = totalCount\n        }\n    }\n    \n    /// 群组名\n    private var groupName: String? {\n        set {\n            if let newValue, !newValue.isEmpty {\n                groupHeader.groupName = newValue\n            } else {\n                groupHeader.groupName = \"default\".localized\n            }\n        }\n        get {\n            return groupHeader.groupName\n        }\n    }\n    \n    var cellData: (groupName: String?, totalCount: Int, messages: [MessageItemModel])? {\n        didSet {\n            groupName = cellData?.groupName ?? \"\"\n            totalCount = cellData?.totalCount ?? 0\n            messages = cellData?.messages ?? []\n        }\n    }\n    \n    /// 折叠事件\n    var showLessAction: (() -> Void)? {\n        get {\n            return groupHeader.showLessAction\n        }\n        set {\n            groupHeader.showLessAction = newValue\n        }\n    }\n    \n    /// 清除群组事件\n    var clearAction: (() -> Void)? {\n        get {\n            return groupHeader.clearAction\n        }\n        set {\n            groupHeader.clearAction = newValue\n        }\n    }\n    \n    var tapAction: ((_ message: MessageItemModel, _ sourceView: MessageItemView) -> Void)? = nil\n\n    /// 查看群组所有消息\n    var showGroupMessageAction: ((_ group: String?) -> Void)? = nil\n    \n    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {\n        super.init(style: style, reuseIdentifier: reuseIdentifier)\n        self.selectionStyle = .none\n        \n        self.contentView.addSubview(panel)\n        panel.addSubview(groupHeader)\n        panel.addSubview(moreView)\n        for view in messageViews.reversed() {\n            panel.addSubview(view)\n        }\n\n        panel.snp.makeConstraints { make in\n            make.edges.equalToSuperview()\n        }\n        \n        groupHeader.snp.remakeConstraints { make in\n            groupHeaderTopConstraint = make.top.equalToSuperview().offset(0).constraint\n            make.left.equalTo(16)\n            make.right.equalTo(-16)\n        }\n        \n        moreView.snp.remakeConstraints { make in\n            make.bottom.equalToSuperview().offset(-18)\n            make.centerX.equalToSuperview()\n        }\n        \n        moreView.addGestureRecognizer(UITapGestureRecognizer())\n        moreView.gestureRecognizers?.first?.rx.event.subscribe(onNext: { [weak self] _ in\n            self?.showGroupMessageAction?(self?.messages.first?.group)\n        }).disposed(by: self.rx.disposeBag)\n       \n        for view in messageViews {\n            view.tapAction = { [weak self] message, sourceView in\n                self?.tapAction?(message, sourceView)\n            }\n        }\n        \n        refreshViewState()\n    }\n    \n    @available(*, unavailable)\n    required init?(coder: NSCoder) {\n        fatalError(\"init(coder:) has not been implemented\")\n    }\n    \n    /// 更新UI\n    private func refreshViewState() {\n        self.messageViews.first?.panel.isUserInteractionEnabled = isExpanded\n        self.contentView.gestureRecognizers?.first?.isEnabled = !isExpanded\n        \n        for (index, view) in messageViews.enumerated() {\n            if isExpanded {\n                view.maskAlpha = 0\n            } else {\n                view.maskAlpha = index == 0 ? 0 : CGFloat(index + 1) * 0.01\n            }\n        }\n    }\n\n    /// 更新布局\n    private func refreshLayout() {\n        // 调整 header 位置\n        groupHeaderTopConstraint?.update(offset: isExpanded ? 0 : 15)\n        \n        // 最大显示 5 条消息， 也就是 messageViews.count\n        let maxCount = min(self.messages.count, self.messageViews.count)\n        \n        // 清除旧的约束\n        for view in messageViews {\n            view.snp.removeConstraints()\n        }\n        \n        for (index, item) in messageViews.enumerated() {\n            if index >= maxCount {\n                break\n            }\n            \n            item.snp.remakeConstraints { make in\n                make.left.right.equalToSuperview()\n                if isExpanded {\n                    item.isShowSubviews = true\n                    \n                    if index == 0 {\n                        make.top.equalTo(groupHeader.snp.bottom).offset(8)\n                    } else {\n                        make.top.equalTo(messageViews[index - 1].snp.bottom).offset(8)\n                    }\n                    if index == maxCount - 1 {\n                        make.bottom.equalTo(moreView.snp.top).offset(-18)\n                    }\n                    item.transform = .identity\n                } else {\n                    if index == 0 {\n                        make.top.equalToSuperview()\n                        \n                        item.isShowSubviews = true\n                        item.transform = .identity\n                        \n                    } else {\n                        // 底部边缘最多额外再显示1条消息\n                        make.bottom.equalTo(messageViews[0].snp.bottom).offset(min(index * 8, 1 * 8))\n                        make.height.equalTo(40)\n                        \n                        item.isShowSubviews = false\n                        // 根据 index 逐渐缩小\n                        let scale = 1 - CGFloat(index) * 0.04\n                        item.transform = CGAffineTransform(scaleX: scale, y: 1)\n                    }\n                    if index == maxCount - 1 {\n                        make.bottom.equalToSuperview().offset(-8)\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "View/MessageList/ShowLessAndClearView.swift",
    "content": "//\n//  ShowLessAndClearView.swift\n//  Bark\n//\n//  Created by huangfeng on 12/23/24.\n//  Copyright © 2024 Fin. All rights reserved.\n//\n\nimport SnapKit\nimport UIKit\n\nclass ShowLessAndClearView: UIView {\n    private let showLessView = ShowLessView()\n    private let clearView = ClearView()\n    \n    private var clearViewWidthConstraint: Constraint? = nil\n    \n    var showLessAction: (() -> Void)?\n    var clearAction: (() -> Void)?\n    \n    init() {\n        super.init(frame: .zero)\n        self.addSubview(showLessView)\n        self.addSubview(clearView)\n        showLessView.snp.makeConstraints { make in\n            make.left.top.bottom.equalToSuperview()\n            make.right.equalTo(-28 - 5)\n        }\n        clearView.snp.makeConstraints { make in\n            make.centerY.right.equalToSuperview()\n            make.height.equalTo(28)\n            self.clearViewWidthConstraint = make.width.equalTo(28).constraint\n        }\n        \n        clearView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(clearTap)))\n        showLessView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(showLesstTap)))\n    }\n\n    @available(*, unavailable)\n    required init?(coder: NSCoder) {\n        fatalError(\"init(coder:) has not been implemented\")\n    }\n    \n    @objc private func clearTap() {\n        if clearView.isExpanded {\n            self.clearAction?()\n            return\n        }\n        self.buttonExpandAnimation(isClearViewExpanded: true)\n    }\n    \n    @objc private func showLesstTap() {\n        if showLessView.isExpanded {\n            self.showLessAction?()\n            return\n        }\n        self.buttonExpandAnimation(isClearViewExpanded: false)\n    }\n    \n    private func buttonExpandAnimation(isClearViewExpanded: Bool) {\n        UIView.animate(withDuration: 0.3) {\n            self.showLessView.isExpanded = !isClearViewExpanded\n            self.clearView.isExpanded = isClearViewExpanded\n            self.clearViewWidthConstraint?.update(offset: isClearViewExpanded ? self.bounds.width - 28 - 5 : 28)\n            self.layoutIfNeeded()\n        }\n    }\n}\n\nprivate class ShowLessView: UIView {\n    let panel: UIView = {\n        let view = UIView()\n        view.backgroundColor = BKColor.grey.lighten5\n        view.layer.cornerRadius = 28 / 2\n        view.clipsToBounds = true\n        return view\n    }()\n        \n    let downArrow: UIImageView = {\n        let imageView = UIImageView(image: UIImage(named: \"baseline_keyboard_arrow_down_black_24pt\")?.withRenderingMode(.alwaysTemplate))\n        imageView.tintColor = BKColor.grey.darken2\n        return imageView\n    }()\n\n    let showLessLabel: UILabel = {\n        let label = UILabel()\n        label.font = UIFont.preferredFont(ofSize: 12)\n        label.textColor = BKColor.grey.darken3\n        label.text = \"showLess\".localized\n        return label\n    }()\n    \n    var isExpanded: Bool = true {\n        didSet {\n            refreshPanelLayout()\n        }\n    }\n\n    init() {\n        super.init(frame: .zero)\n        self.addSubview(panel)\n        panel.addSubview(downArrow)\n        panel.addSubview(showLessLabel)\n        \n        downArrow.snp.makeConstraints { make in\n            make.left.equalTo(self).offset(2)\n            make.top.equalTo(self).offset(2)\n            make.bottom.equalTo(self).offset(-2)\n            make.width.height.equalTo(24)\n        }\n        \n        showLessLabel.snp.makeConstraints { make in\n            make.left.equalTo(downArrow.snp.right).offset(2)\n            make.centerY.equalTo(self)\n            make.right.equalTo(self).offset(-10)\n        }\n        \n        refreshPanelLayout()\n    }\n\n    @available(*, unavailable)\n    required init?(coder: NSCoder) {\n        fatalError(\"init(coder:) has not been implemented\")\n    }\n    \n    func refreshPanelLayout() {\n        // 收缩时，只收缩 panel ，控件实际宽度不变，主要用于动画\n        panel.snp.remakeConstraints { make in\n            make.left.top.bottom.equalToSuperview()\n            if isExpanded {\n                make.right.equalToSuperview()\n            } else {\n                make.width.equalTo(self.snp.height)\n            }\n        }\n    }\n}\n\nprivate class ClearView: UIView {\n    let clearIcon: UIImageView = {\n        let imageView = UIImageView(image: UIImage(named: \"baseline_close_white_48pt\")?.withRenderingMode(.alwaysTemplate))\n        imageView.tintColor = BKColor.grey.darken2\n        imageView.contentMode = .scaleAspectFit\n        return imageView\n    }()\n\n    let clearLabel: UILabel = {\n        let label = UILabel()\n        label.font = UIFont.preferredFont(ofSize: 12)\n        label.textColor = BKColor.grey.darken3\n        label.text = \"clear\".localized\n        label.alpha = 0\n        return label\n    }()\n    \n    var isExpanded: Bool = false {\n        didSet {\n            self.clearLabel.alpha = self.isExpanded ? 1 : 0\n            self.clearIcon.alpha = self.isExpanded ? 0 : 1\n        }\n    }\n    \n    init() {\n        super.init(frame: .zero)\n        self.backgroundColor = BKColor.grey.lighten5\n        self.layer.cornerRadius = 28 / 2\n        self.clipsToBounds = true\n        \n        self.addSubview(clearLabel)\n        self.addSubview(clearIcon)\n        \n        clearIcon.snp.remakeConstraints { make in\n            make.right.equalToSuperview().offset(-4)\n            make.top.equalToSuperview().offset(4)\n            make.bottom.equalToSuperview().offset(-4)\n            make.width.height.equalTo(20)\n        }\n        \n        clearLabel.snp.remakeConstraints { make in\n            make.center.equalToSuperview()\n        }\n    }\n    \n    @available(*, unavailable)\n    required init?(coder: NSCoder) {\n        fatalError(\"init(coder:) has not been implemented\")\n    }\n}\n"
  },
  {
    "path": "View/MessageSettingFooter.swift",
    "content": "//\n//  MessageSettingFooter.swift\n//  Bark\n//\n//  Created by huangfeng on 11/14/24.\n//  Copyright © 2024 Fin. All rights reserved.\n//\n\nimport UIKit\n\nclass MessageSettingFooter: UITextView, UITextViewDelegate {\n    init() {\n        super.init(frame: CGRect(x: 0, y: 0, width: 0, height: 80), textContainer: .none)\n        self.backgroundColor = UIColor.clear\n        self.isEditable = false\n        self.delegate = self\n        \n        // 版本号\n        let appVersion = Bundle.main.infoDictionary?[\"CFBundleShortVersionString\"] as? String ?? \"\"\n        // build号\n        let buildVersion = Bundle.main.infoDictionary?[\"CFBundleVersion\"] as? String ?? \"\"\n        \n        let attr = NSMutableAttributedString(string: \"\\(\"version\".localized) \\(appVersion) (\\(buildVersion))\\n\", attributes: [.font: UIFont.preferredFont(ofSize: 12), .foregroundColor: BKColor.grey.darken1])\n        attr.append(NSAttributedString(string: \"privacyPolicy\".localized, attributes: [.link: \"privacyPolicy\"]))\n        attr.append(NSAttributedString(string: \"  ·  \"))\n        attr.append(NSAttributedString(string: \"userAgreement\".localized, attributes: [.link: \"userAgreement\"]))\n        attr.append(NSAttributedString(string: \"  ·  \"))\n        attr.append(NSAttributedString(string: \"restoreSubscription\".localized, attributes: [.link: \"restoreSubscription\"]))\n        \n        let style = NSMutableParagraphStyle()\n        style.lineSpacing = 6\n        attr.addAttribute(.paragraphStyle, value: style, range: NSRange(location: 0, length: attr.length))\n        \n        self.attributedText = attr\n        self.linkTextAttributes = [.foregroundColor: BKColor.grey.darken1, .underlineStyle: NSUnderlineStyle.single.rawValue, .font: UIFont.preferredFont(ofSize: 12)]\n        self.textAlignment = .center\n    }\n\n    @available(*, unavailable)\n    required init?(coder: NSCoder) {\n        fatalError(\"init(coder:) has not been implemented\")\n    }\n    \n    var openLinkHandler: ((String) -> Void)?\n    func textView(_ textView: UITextView, shouldInteractWith url: URL, in characterRange: NSRange) -> Bool {\n        self.openLinkHandler?(url.absoluteString)\n        return false\n    }\n}\n"
  },
  {
    "path": "View/MutableTextCell.swift",
    "content": "//\n//  DeviceTokenCell.swift\n//  Bark\n//\n//  Created by huangfeng on 2022/3/23.\n//  Copyright © 2022 Fin. All rights reserved.\n//\n\nimport UIKit\n\nclass MutableTextCell: BaseTableViewCell<MutableTextCellViewModel> {\n\n    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {\n        super.init(style: UITableViewCell.CellStyle.value1, reuseIdentifier: reuseIdentifier)\n        self.selectionStyle = .none\n        self.accessoryType = .none\n        self.backgroundColor = BKColor.background.secondary\n        self.detailTextLabel?.textColor = BKColor.grey.darken2\n    }\n\n    @available(*, unavailable)\n    required init?(coder aDecoder: NSCoder) {\n        fatalError(\"init(coder:) has not been implemented\")\n    }\n    \n    override func bindViewModel(model: MutableTextCellViewModel) {\n        super.bindViewModel(model: model)\n        \n        self.textLabel?.text = model.title\n        model.text\n            .drive(self.detailTextLabel!.rx.text)\n            .disposed(by: rx.reuseBag)\n    }\n}\n"
  },
  {
    "path": "View/MutableTextCellViewModel.swift",
    "content": "//\n//  DeviceTokenCellViewModel.swift\n//  Bark\n//\n//  Created by huangfeng on 2022/3/23.\n//  Copyright © 2022 Fin. All rights reserved.\n//\n\nimport RxCocoa\nimport UIKit\nclass MutableTextCellViewModel: ViewModel {\n    var title: String\n    var text: Driver<String>\n    init(title: String, text: Driver<String>) {\n        self.title = title\n        self.text = text\n        super.init()\n    }\n}\n"
  },
  {
    "path": "View/PreviewCardCell.swift",
    "content": "//\n//  PreviewCardCell.swift\n//  Bark\n//\n//  Created by huangfeng on 2018/6/26.\n//  Copyright © 2018 Fin. All rights reserved.\n//\n\nimport Material\nimport UIKit\n\nclass PreviewCardCell: BaseTableViewCell<PreviewCardCellViewModel> {\n    let previewButton: IconButton = {\n        let button = IconButton(image: Icon.cm.skipForward, tintColor: BKColor.grey.base)\n        button.accessibilityLabel = \"demo\".localized\n        return button\n    }()\n    \n    let copyButton: IconButton = {\n        let button = IconButton(image: UIImage(named: \"baseline_file_copy_white_24pt\"), tintColor: BKColor.grey.base)\n        button.accessibilityLabel = \"Copy\".localized\n        return button\n    }()\n    \n    let titleLabel: UILabel = {\n        let label = UILabel()\n        label.font = UIFont.preferredFont(ofSize: 14)\n        label.adjustsFontForContentSizeCategory = true\n        label.textColor = BKColor.grey.darken3\n        label.numberOfLines = 0\n        return label\n    }()\n\n    let bodyLabel: UILabel = {\n        let label = UILabel()\n        label.font = UIFont.preferredFont(ofSize: 14)\n        label.adjustsFontForContentSizeCategory = true\n        label.textColor = BKColor.grey.darken2\n        label.numberOfLines = 0\n        return label\n    }()\n    \n    let noticeLabel: UILabel = {\n        let label = UILabel()\n        label.font = UIFont.preferredFont(ofSize: 12)\n        label.adjustsFontForContentSizeCategory = true\n        label.textColor = BKColor.grey.base\n        label.numberOfLines = 0\n        label.isUserInteractionEnabled = true\n        return label\n    }()\n\n    let contentImageView: UIImageView = {\n        let imageView = UIImageView()\n        imageView.contentMode = .scaleAspectFit\n        return imageView\n    }()\n\n    let card: UIView = {\n        let view = UIView()\n        view.backgroundColor = BKColor.background.secondary\n        view.layer.cornerRadius = 10\n        view.clipsToBounds = true\n        return view\n    }()\n    \n    var copyHandler: (() -> Void)?\n    \n    let contentLabel: UILabel = {\n        let label = UILabel()\n        label.lineBreakMode = .byCharWrapping\n        label.numberOfLines = 0\n        label.font = UIFont.preferredFont(ofSize: 14)\n        label.adjustsFontForContentSizeCategory = true\n        return label\n    }()\n    \n    @available(*, unavailable)\n    required init?(coder aDecoder: NSCoder) {\n        fatalError(\"init(coder:) has not been implemented\")\n    }\n    \n    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {\n        super.init(style: style, reuseIdentifier: reuseIdentifier)\n        self.selectionStyle = .none\n        self.backgroundColor = BKColor.background.primary\n        \n        contentView.addSubview(card)\n        card.addSubview(copyButton)\n        card.addSubview(previewButton)\n        \n        card.snp.makeConstraints { make in\n            make.left.top.equalToSuperview().offset(16)\n            make.right.equalToSuperview().offset(-16)\n            make.bottom.equalToSuperview()\n        }\n        previewButton.snp.makeConstraints { make in\n            make.right.equalToSuperview().offset(-10)\n            make.centerY.equalTo(card.snp.top).offset(40)\n            make.width.height.equalTo(40)\n        }\n        copyButton.snp.makeConstraints { make in\n            make.right.equalTo(previewButton.snp.left).offset(-10)\n            make.centerY.equalTo(previewButton)\n            make.width.height.equalTo(40)\n        }\n        \n        let titleStackView = UIStackView()\n        titleStackView.axis = .vertical\n        titleStackView.isLayoutMarginsRelativeArrangement = true\n        titleStackView.layoutMargins = UIEdgeInsets(top: 0, left: 15, bottom: 0, right: 0)\n        titleStackView.addArrangedSubview(titleLabel)\n        titleStackView.addArrangedSubview(bodyLabel)\n        \n        card.addSubview(titleStackView)\n        \n        titleStackView.snp.makeConstraints { make in\n            make.centerY.equalTo(copyButton)\n            make.left.equalToSuperview()\n            make.right.equalTo(copyButton.snp.left)\n        }\n\n        let contentStackView = UIStackView()\n        contentStackView.axis = .vertical\n        contentStackView.spacing = 20\n        contentStackView.setCustomSpacing(10, after: contentLabel)\n        card.addSubview(contentStackView)\n        \n        contentStackView.addArrangedSubview(contentImageView)\n        contentStackView.addArrangedSubview(InsetView(subView: contentLabel, insets: .init(top: 0, left: 12, bottom: 0, right: 12)))\n        contentStackView.addArrangedSubview(InsetView(subView: noticeLabel, insets: .init(top: 0, left: 10, bottom: 0, right: 10)))\n        \n        contentStackView.snp.makeConstraints { make in\n            make.left.right.equalToSuperview()\n            make.top.equalTo(previewButton.snp.bottom).offset(20)\n            make.bottom.equalToSuperview().offset(-10)\n        }\n        \n        noticeLabel.addGestureRecognizer(UITapGestureRecognizer())\n    }\n    \n    override func bindViewModel(model: PreviewCardCellViewModel) {\n        super.bindViewModel(model: model)\n        \n        model.title\n            .bind(to: self.titleLabel.rx.text).disposed(by: rx.reuseBag)\n        model.body\n            .bind(to: self.bodyLabel.rx.text).disposed(by: rx.reuseBag)\n        model.content\n            .bind(to: self.contentLabel.rx.attributedText).disposed(by: rx.reuseBag)\n        model.notice\n            .bind(to: self.noticeLabel.rx.attributedText).disposed(by: rx.reuseBag)\n        model.contentImage\n            .compactMap { $0 }\n            .bind(to: self.contentImageView.rx.image)\n            .disposed(by: rx.reuseBag)\n        model.contentImage\n            .map { $0 == nil }\n            .bind(to: self.contentImageView.rx.isHidden)\n            .disposed(by: rx.reuseBag)\n        \n        // 点击通知\n        noticeLabel.gestureRecognizers!.first!\n            .rx.event\n            .compactMap { [weak weakModel = viewModel] _ -> ViewModel? in\n                // 仅在有 moreViewModel 时 点击\n                weakModel?.previewModel.moreViewModel\n            }\n            .bind(to: model.noticeTap)\n            .disposed(by: rx.reuseBag)\n        \n        // 点击复制\n        copyButton.rx.tap.map { [weak self] () -> String in\n            self?.contentLabel.text ?? \"\"\n        }\n        .bind(to: model.copy)\n        .disposed(by: rx.reuseBag)\n        \n        // 点击预览\n        previewButton.rx.tap.compactMap { [weak self] () -> URL? in\n            if let urlStr = self?.contentLabel.text?.urlEncoded(),\n               let url = URL(string: urlStr)\n            {\n                return url\n            }\n            return nil\n        }\n        .bind(to: model.preview)\n        .disposed(by: rx.reuseBag)\n    }\n}\n"
  },
  {
    "path": "View/PreviewCardCellViewModel.swift",
    "content": "//\n//  PreviewCardCellViewModel.swift\n//  Bark\n//\n//  Created by huangfeng on 2020/11/23.\n//  Copyright © 2020 Fin. All rights reserved.\n//\n\nimport Foundation\nimport Material\nimport RxCocoa\nclass PreviewCardCellViewModel: ViewModel {\n    let title = BehaviorRelay(value: \"\")\n    let body = BehaviorRelay(value: \"\")\n    let content = BehaviorRelay(value: NSAttributedString())\n    let notice = BehaviorRelay(value: NSAttributedString())\n    let contentImage: BehaviorRelay<UIImage?>\n    \n    let noticeTap = PublishRelay<ViewModel>()\n    let copy = PublishRelay<String>()\n    let preview = PublishRelay<URL>()\n    \n    let previewModel: PreviewModel\n    init(previewModel: PreviewModel, clientState: Driver<Client.ClienState>) {\n        self.previewModel = previewModel\n        contentImage = BehaviorRelay<UIImage?>(value: previewModel.image)\n        \n        super.init()\n        \n        if let modelTitle = previewModel.title {\n            title.accept(modelTitle)\n        }\n        \n        if let modelBody = previewModel.body {\n            body.accept(modelBody)\n        }\n        \n        // client State 更改时，重新生成 content\n        // 因为这时可能 ServerManager.shared.currentAddress 或 Client.shared.key 发生了改变。\n        // 这不是一个好的写法，viewModel 应尽可能只依赖固定的 input ，而不应依赖不可预测的外部变量（ currentAddress 与 key ）。\n        // 但这个项目是由 MVC 临时重构为 MVVM ，之前是这样写的，所以懒得改动了。\n        clientState.compactMap { [weak self] _ -> NSAttributedString? in\n            self?.contentAttrStr()\n        }\n        .drive(content)\n        .disposed(by: rx.disposeBag)\n        \n        let noticeStr = \"\\(previewModel.notice ?? \"\")\"\n        let noticeAttrStr = NSMutableAttributedString(string: noticeStr, attributes: [\n            NSAttributedString.Key.foregroundColor: BKColor.grey.base,\n            NSAttributedString.Key.font: UIFont.preferredFont(ofSize: 12)\n        ])\n        \n        if let moreInfo = previewModel.moreInfo {\n            noticeAttrStr.append(NSMutableAttributedString(string: \"   \\(moreInfo)\", attributes: [\n                NSAttributedString.Key.foregroundColor: BKColor.blue.base,\n                NSAttributedString.Key.font: UIFont.preferredFont(ofSize: 12)\n            ]))\n        }\n        notice.accept(noticeAttrStr)\n    }\n    \n    func contentAttrStr() -> NSAttributedString {\n        var fontSize: CGFloat = 14\n        if UIScreen.main.bounds.size.width <= 320 {\n            fontSize = 11\n        }\n        let serverUrl = URL(string: ServerManager.shared.currentServer.address)!\n        let attrStr = NSMutableAttributedString(string: \"\")\n        attrStr.append(NSAttributedString(string: serverUrl.absoluteString, attributes: [\n            NSAttributedString.Key.foregroundColor: BKColor.grey.darken4,\n            NSAttributedString.Key.font: UIFont.preferredFont(ofSize: fontSize)\n        ]))\n        let key = ServerManager.shared.currentServer.key\n        attrStr.append(NSAttributedString(string: \"/\\(key.count > 0 ? key : \"Your Key\")\", attributes: [\n            NSAttributedString.Key.foregroundColor: BKColor.grey.darken3,\n            NSAttributedString.Key.font: UIFont.preferredFont(ofSize: fontSize)\n        ]))\n        \n        if let modelTitle = previewModel.title {\n            attrStr.append(NSAttributedString(string: \"/\\(modelTitle)\", attributes: [\n                NSAttributedString.Key.foregroundColor: BKColor.grey.darken1,\n                NSAttributedString.Key.font: UIFont.preferredFont(ofSize: fontSize)\n            ]))\n        }\n        if let modelBody = previewModel.body {\n            attrStr.append(NSAttributedString(string: \"/\\(modelBody)\", attributes: [\n                NSAttributedString.Key.foregroundColor: BKColor.grey.base,\n                NSAttributedString.Key.font: UIFont.preferredFont(ofSize: fontSize)\n            ]))\n        }\n        if let queryParameter = previewModel.queryParameter {\n            attrStr.append(NSAttributedString(string: \"?\\(queryParameter)\", attributes: [\n                NSAttributedString.Key.foregroundColor: BKColor.grey.lighten1,\n                NSAttributedString.Key.font: UIFont.preferredFont(ofSize: fontSize)\n            ]))\n        }\n        \n        return attrStr\n    }\n}\n"
  },
  {
    "path": "View/ServerListTableViewCell.swift",
    "content": "//\n//  ServerListTableViewCell.swift\n//  Bark\n//\n//  Created by huangfeng on 2022/4/1.\n//  Copyright © 2022 Fin. All rights reserved.\n//\n\nimport Material\nimport UIKit\n\nclass ServerListTableViewCell: BaseTableViewCell<ServerListTableViewCellViewModel> {\n    let backgroundPanel: UIView = {\n        let view = UIView()\n        view.layer.cornerRadius = 3\n        view.clipsToBounds = true\n        view.backgroundColor = BKColor.background.secondary\n\n        view.clipsToBounds = true\n        view.layer.borderColor = BKColor.grey.lighten3.cgColor\n        view.layer.borderWidth = 1\n        return view\n    }()\n\n    let addressLabel: UILabel = {\n        let label = UILabel()\n        label.font = UIFont.preferredFont(ofSize: 14, weight: .medium)\n        label.adjustsFontForContentSizeCategory = true\n        label.textColor = BKColor.grey.darken4\n        label.numberOfLines = 0\n        label.lineBreakMode = .byCharWrapping\n        return label\n    }()\n\n    let keyLabel: UILabel = {\n        let label = UILabel()\n        label.font = UIFont.preferredFont(ofSize: 12)\n        label.adjustsFontForContentSizeCategory = true\n        label.textColor = BKColor.grey.darken4\n        label.numberOfLines = 0\n        return label\n    }()\n\n    let stateImageView: UIImageView = {\n        let imageView = UIImageView()\n        imageView.contentMode = .scaleAspectFit\n        imageView.layer.cornerRadius = 15\n        imageView.clipsToBounds = true\n        return imageView\n    }()\n\n    var state: Bool = false {\n        didSet {\n            if state {\n                stateImageView.image = UIImage(named: \"online\")\n            } else {\n                stateImageView.image = UIImage(named: \"offline\")\n            }\n        }\n    }\n\n    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {\n        super.init(style: style, reuseIdentifier: reuseIdentifier)\n        self.selectionStyle = .none\n        self.backgroundColor = BKColor.background.primary\n\n        addSubview(backgroundPanel)\n        addSubview(stateImageView)\n        addSubview(addressLabel)\n        addSubview(keyLabel)\n\n        backgroundPanel.snp.makeConstraints { make in\n            make.left.equalToSuperview().offset(18)\n            make.right.equalToSuperview().offset(-18)\n            make.top.equalToSuperview().offset(5)\n            make.bottom.equalToSuperview().offset(-5)\n        }\n\n        stateImageView.snp.makeConstraints { make in\n            make.centerY.equalTo(backgroundPanel)\n            make.left.equalTo(backgroundPanel).offset(13)\n            make.width.height.equalTo(30)\n        }\n        addressLabel.snp.makeConstraints { make in\n            make.left.equalTo(stateImageView.snp.right).offset(8)\n            make.top.equalTo(backgroundPanel).offset(10)\n            make.right.equalTo(backgroundPanel).offset(-18)\n        }\n        keyLabel.snp.makeConstraints { make in\n            make.top.equalTo(addressLabel.snp.bottom).offset(1)\n            make.left.right.equalTo(addressLabel)\n            make.bottom.equalTo(backgroundPanel).offset(-10)\n        }\n    }\n\n    @available(*, unavailable)\n    required init?(coder: NSCoder) {\n        fatalError(\"init(coder:) has not been implemented\")\n    }\n\n    override func bindViewModel(model: ServerListTableViewCellViewModel) {\n        super.bindViewModel(model: model)\n\n        model.name\n            .bind(to: addressLabel.rx.text)\n            .disposed(by: rx.reuseBag)\n\n        model.key\n            .bind(to: keyLabel.rx.text)\n            .disposed(by: rx.reuseBag)\n\n        model.state\n            .subscribe { state in\n                self.state = state\n            } onError: { _ in }\n            .disposed(by: rx.reuseBag)\n    }\n\n    override func layoutSubviews() {\n        super.layoutSubviews()\n        self.backgroundPanel.layer.cornerRadius = self.backgroundPanel.bounds.height / 2\n    }\n}\n"
  },
  {
    "path": "View/ServerListTableViewCellViewModel.swift",
    "content": "//\n//  ServerListTableViewCellViewModel.swift\n//  Bark\n//\n//  Created by huangfeng on 2022/4/1.\n//  Copyright © 2022 Fin. All rights reserved.\n//\n\nimport RxRelay\nimport UIKit\n\nclass ServerListTableViewCellViewModel: ViewModel {\n    let server: Server\n    \n    let name: BehaviorRelay<String>\n    let key: BehaviorRelay<String>\n    let state: BehaviorRelay<Bool>\n    \n    init(server: Server) {\n        self.server = server\n        \n        self.name = BehaviorRelay<String>(value: {\n            var serverName = URL(string: server.address)?.host ?? \"Invalid Server\"\n            if let name = server.name, !name.isEmpty {\n                serverName = name + \"\\n\" + serverName\n            }\n            return serverName\n        }())\n        self.key = BehaviorRelay<String>(value: !server.key.isEmpty ? server.key : \"none\")\n        self.state = BehaviorRelay<Bool>(value: server.state == .ok)\n        \n        super.init()\n    }\n}\n"
  },
  {
    "path": "View/SettingSectionHeader.swift",
    "content": "//\n//  SettingSectionHeader.swift\n//  Bark\n//\n//  Created by huangfeng on 11/13/24.\n//  Copyright © 2024 Fin. All rights reserved.\n//\n\nimport UIKit\n\nclass SettingSectionHeader: UIView {\n    let titleLabel: UILabel = {\n        let label = UILabel()\n        label.textColor = BKColor.grey.darken1\n        label.font = UIFont.preferredFont(ofSize: 12)\n        label.numberOfLines = 0\n        return label\n    }()\n\n    init() {\n        super.init(frame: CGRect.zero)\n        self.addSubview(titleLabel)\n        titleLabel.snp.makeConstraints { make in\n            make.top.equalTo(12)\n            make.bottom.equalTo(-12)\n            make.left.equalTo(13)\n            make.right.equalTo(-13)\n        }\n    }\n\n    @available(*, unavailable)\n    required init?(coder: NSCoder) {\n        fatalError(\"init(coder:) has not been implemented\")\n    }\n}\n\nclass SettingSectionFooter: UIView {\n    let titleLabel: UILabel = {\n        let label = UILabel()\n        label.textColor = BKColor.grey.darken1\n        label.font = UIFont.preferredFont(ofSize: 12)\n        label.numberOfLines = 0\n        return label\n    }()\n\n    init() {\n        super.init(frame: CGRect.zero)\n        self.addSubview(titleLabel)\n        titleLabel.snp.makeConstraints { make in\n            make.top.equalTo(8)\n//            make.bottom.equalTo(-6)\n            make.left.equalTo(12)\n            make.right.equalTo(-12)\n        }\n    }\n\n    @available(*, unavailable)\n    required init?(coder: NSCoder) {\n        fatalError(\"init(coder:) has not been implemented\")\n    }\n}\n"
  },
  {
    "path": "View/SoundCell.swift",
    "content": "//\n//  SoundCell.swift\n//  Bark\n//\n//  Created by huangfeng on 2020/9/14.\n//  Copyright © 2020 Fin. All rights reserved.\n//\n\nimport AVKit\nimport Material\nimport UIKit\n\nclass SoundCell: BaseTableViewCell<SoundCellViewModel> {\n    let copyButton: IconButton = {\n        let button = IconButton(image: UIImage(named: \"baseline_file_copy_white_24pt\"), tintColor: BKColor.grey.base)\n        button.accessibilityLabel = \"Copy\".localized\n        return button\n    }()\n    \n    let nameLabel: UILabel = {\n        let label = UILabel()\n        label.fontSize = 14\n        label.textColor = BKColor.grey.darken4\n        return label\n    }()\n\n    let durationLabel: UILabel = {\n        let label = UILabel()\n        label.fontSize = 12\n        label.textColor = BKColor.grey.darken1\n        return label\n    }()\n\n    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {\n        super.init(style: style, reuseIdentifier: reuseIdentifier)\n        self.selectionStyle = .none\n        self.backgroundColor = BKColor.background.secondary\n        self.contentView.addSubview(nameLabel)\n        self.contentView.addSubview(durationLabel)\n        self.contentView.addSubview(copyButton)\n        \n        nameLabel.snp.makeConstraints { make in\n            make.left.top.equalToSuperview().offset(15)\n        }\n        durationLabel.snp.makeConstraints { make in\n            make.left.equalTo(nameLabel)\n            make.top.equalTo(nameLabel.snp.bottom).offset(5)\n            make.bottom.equalToSuperview().offset(-15)\n        }\n        copyButton.snp.makeConstraints { make in\n            make.right.equalToSuperview().offset(-15)\n            make.centerY.equalToSuperview()\n            make.width.height.equalTo(40)\n        }\n    }\n\n    @available(*, unavailable)\n    required init?(coder aDecoder: NSCoder) {\n        fatalError(\"init(coder:) has not been implemented\")\n    }\n    \n    override func bindViewModel(model: SoundCellViewModel) {\n        super.bindViewModel(model: model)\n\n        model.name\n            .bind(to: nameLabel.rx.text)\n            .disposed(by: rx.reuseBag)\n        model.duration\n            .map { String(format: \"%.2g second(s)\", CMTimeGetSeconds($0)) }\n            .bind(to: durationLabel.rx.text)\n            .disposed(by: rx.reuseBag)\n        \n        copyButton.rx.tap\n            .map { model.name.value }\n            .bind(to: model.copyNameAction)\n            .disposed(by: rx.reuseBag)\n    }\n}\n"
  },
  {
    "path": "View/SoundCellViewModel.swift",
    "content": "//\n//  SoundCellViewModel.swift\n//  Bark\n//\n//  Created by huangfeng on 2020/11/17.\n//  Copyright © 2020 Fin. All rights reserved.\n//\n\nimport AVKit\nimport Foundation\nimport RxCocoa\nimport RxSwift\n\nclass SoundCellViewModel: ViewModel {\n    let name = BehaviorRelay<String>(value: \"\")\n    let duration = BehaviorRelay<CMTime>(value: .zero)\n\n    let copyNameAction = PublishRelay<String>()\n    let playAction = PublishRelay<CFURL>()\n\n    let model: AVURLAsset\n    init(model: AVURLAsset) {\n        self.model = model\n        name.accept(model.url.deletingPathExtension().lastPathComponent)\n        duration.accept(model.duration)\n    }\n}\n"
  },
  {
    "path": "View/SpacerCell.swift",
    "content": "//\n//  SpacerCell.swift\n//  Bark\n//\n//  Created by huangfeng on 2021/6/25.\n//  Copyright © 2021 Fin. All rights reserved.\n//\n\nimport UIKit\n\nclass SpacerCell: UITableViewCell {\n    var height: CGFloat = 0 {\n        didSet {\n            self.contentView.snp.remakeConstraints { make in\n                make.height.equalTo(height)\n            }\n        }\n    }\n\n    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {\n        super.init(style: style, reuseIdentifier: reuseIdentifier)\n        self.backgroundColor = UIColor.clear\n        self.selectionStyle = .none\n    }\n\n    @available(*, unavailable)\n    required init?(coder aDecoder: NSCoder) {\n        fatalError(\"init(coder:) has not been implemented\")\n    }\n}\n"
  },
  {
    "path": "View/TextCell.swift",
    "content": "//\n//  TextCell.swift\n//  Bark\n//\n//  Created by huangfeng on 2021/6/25.\n//  Copyright © 2021 Fin. All rights reserved.\n//\n\nimport UIKit\n\nclass DetailTextCell: UITableViewCell {\n    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {\n        super.init(style: .value1, reuseIdentifier: reuseIdentifier)\n        self.selectionStyle = .none\n        self.accessoryType = .disclosureIndicator\n        self.backgroundColor = BKColor.background.secondary\n    }\n\n    @available(*, unavailable)\n    required init?(coder aDecoder: NSCoder) {\n        fatalError(\"init(coder:) has not been implemented\")\n    }\n}\n"
  },
  {
    "path": "View/UINavigationItem+Extension.swift",
    "content": "//\n//  UINavigationItem+Extension.swift\n//  Bark\n//\n//  Created by huangfeng on 2020/9/23.\n//  Copyright © 2020 Fin. All rights reserved.\n//\n\nimport UIKit\n\n// 如果第一个 item 是系统自带的 UIBarButtonItem ，则距离导航栏左右距离只有8\n// 自己定义的的则最少有16，太宽了\n// 所以先用 一个 fixedSpace UIBarButtonItem 先把距离给缩短点，\n// 然后用个 AlignmentRectInsetsOverridable 把自己的按钮往 左/右 挪动，减少距离\n// 用 HitTestSlopable 增加点击区域\nenum UINavigationItemPosition {\n    case left\n    case right\n}\n\nextension UINavigationItem {\n    func setLeftBarButtonItem(item: UIBarButtonItem) {\n        setBarButtonItems(items: [item], position: .left)\n    }\n\n    func setRightBarButtonItem(item: UIBarButtonItem) {\n        setBarButtonItems(items: [item], position: .right)\n    }\n\n    func setBarButtonItems(items: [UIBarButtonItem], position: UINavigationItemPosition) {\n        if #available(iOS 26.0, *) {\n            // iOS 26 之后的版本，不再微调间距\n            if position == .left {\n                self.leftBarButtonItems = items\n            } else {\n                self.rightBarButtonItems = items\n            }\n            return\n        }\n        \n        guard items.count > 0 else {\n            self.leftBarButtonItems = nil\n            return\n        }\n        var buttonItems = items\n        if #available(iOS 11.0, *) {\n            for item in buttonItems {\n                guard let view = item.customView else { continue }\n                item.customView?.translatesAutoresizingMaskIntoConstraints = false\n                (item.customView as? HitTestSlopable)?.hitTestSlop = UIEdgeInsets(top: -10, left: -10, bottom: -10, right: -10)\n                (item.customView as? AlignmentRectInsetsOverridable)?.alignmentRectInsetsOverride = UIEdgeInsets(top: 0, left: position == .left ? 8 : -8, bottom: 0, right: position == .left ? -8 : 8)\n                item.customView?.snp.makeConstraints { make in\n                    make.width.equalTo(view.bounds.size.width > 24 ? view.bounds.width : 24)\n                    make.height.equalTo(view.bounds.size.height > 24 ? view.bounds.height : 24)\n                }\n            }\n            buttonItems.insert(UIBarButtonItem(barButtonSystemItem: .fixedSpace, target: nil, action: nil), at: 0)\n        } else {\n            let spacer = UIBarButtonItem(barButtonSystemItem: .fixedSpace, target: nil, action: nil)\n            spacer.width = -8\n            buttonItems.insert(spacer, at: 0)\n        }\n        if position == .left {\n            self.leftBarButtonItems = buttonItems\n        } else {\n            self.rightBarButtonItems = buttonItems\n        }\n    }\n}\n"
  },
  {
    "path": "check_unused_translations.py",
    "content": "#!/usr/bin/env python3\n\"\"\"\nBark项目本地化字符串分析工具\n\n这个脚本会扫描整个Bark项目，找出 Localizable.xcstrings 中未使用的翻译key。\n检测方式：\n1. 任何在双引号内且在本地化文件中定义的字符串都会被认为是被使用的key\n2. \"key\".localized 和 \"key\".localized(with:) 模式\n\n使用方法:\n    python3 check_unused_translations.py\n    \n输出:\n    - 控制台显示分析结果\n\"\"\"\n\nimport json\nimport os\nimport re\nimport sys\nfrom pathlib import Path\n\nclass BarkLocalizationAnalyzer:\n    def __init__(self, project_root):\n        self.project_root = Path(project_root)\n        self.localization_file = self.project_root / \"Bark\" / \"Localizable.xcstrings\"\n        \n    def extract_all_keys(self):\n        \"\"\"提取 Localizable.xcstrings 中的所有key\"\"\"\n        try:\n            with open(self.localization_file, 'r', encoding='utf-8') as f:\n                data = json.load(f)\n            return set(data['strings'].keys())\n        except Exception as e:\n            print(f\"❌ 读取本地化文件失败: {e}\")\n            return set()\n    \n    def find_swift_files(self):\n        \"\"\"查找所有Swift源码文件\"\"\"\n        swift_files = []\n        for path in self.project_root.rglob(\"*.swift\"):\n            # 跳过Pods和build目录\n            if \"Pods\" not in str(path) and \"build\" not in str(path):\n                swift_files.append(path)\n        return swift_files\n    \n    def extract_used_keys_from_file(self, file_path, all_defined_keys):\n        \"\"\"从Swift文件中提取使用的本地化key\"\"\"\n        used_keys = set()\n        localized_keys = set()  # 收集.localized中的key\n        \n        try:\n            with open(file_path, 'r', encoding='utf-8') as f:\n                content = f.read()\n            \n            # 方法1: 查找所有在双引号内的字符串，包括多行和转义字符\n            quoted_strings = re.findall(r'\"([^\"]*)\"', content, re.MULTILINE | re.DOTALL)\n            \n            # 检查哪些引号内的字符串是已定义的本地化key\n            for quoted_string in quoted_strings:\n                # 去掉前后空白字符\n                quoted_string = quoted_string.strip()\n                if quoted_string and quoted_string in all_defined_keys:\n                    used_keys.add(quoted_string)\n            \n            # 方法2: 查找 \"key\".localized 和 \"key\".localized(with:) 模式\n            localized_patterns = [\n                r'\"([^\"]+)\"\\s*\\.\\s*localized\\b',                    # \"key\".localized\n                r'\"([^\"]+)\"\\s*\\.\\s*localized\\s*\\(\\s*with:',         # \"key\".localized(with:\n                r'\\'([^\\']+)\\'\\s*\\.\\s*localized\\b',                 # 'key'.localized\n                r'\\'([^\\']+)\\'\\s*\\.\\s*localized\\s*\\(\\s*with:',      # 'key'.localized(with:\n            ]\n            \n            for pattern in localized_patterns:\n                matches = re.findall(pattern, content, re.MULTILINE | re.DOTALL)\n                for match in matches:\n                    match = match.strip()\n                    if match:\n                        localized_keys.add(match)  # 收集所有.localized中的key\n                        if match in all_defined_keys:\n                            used_keys.add(match)\n            \n        except Exception as e:\n            print(f\"⚠️  读取文件失败 {file_path}: {e}\")\n        \n        return used_keys, localized_keys\n    \n    def find_all_used_keys(self, all_defined_keys):\n        \"\"\"在整个项目中查找所有使用的本地化 key\"\"\"\n        # 查找所有Swift文件\n        swift_files = self.find_swift_files()\n        print(f\"📁 找到 {len(swift_files)} 个Swift文件\")\n        \n        # 提取使用的key\n        used_keys = set()\n        all_localized_keys = set()  # 收集所有.localized中的key\n        files_with_keys = 0\n        \n        for file_path in swift_files:\n            file_keys, localized_keys = self.extract_used_keys_from_file(file_path, all_defined_keys)\n            if file_keys:\n                files_with_keys += 1\n                used_keys.update(file_keys)\n            all_localized_keys.update(localized_keys)\n        \n        print(f\"🔑 在 {files_with_keys} 个文件中找到 {len(used_keys)} 个使用的key\")\n        \n        # 计算在代码中使用但未在本地化文件中定义的key\n        missing_in_localization = all_localized_keys - all_defined_keys\n        \n        return used_keys, files_with_keys, missing_in_localization\n    \n    def analyze(self):\n        \"\"\"执行完整的本地化分析\"\"\"\n        print(\"🔍 开始分析Bark项目的本地化使用情况...\")\n        \n        # 提取所有定义的key\n        print(\"📖 读取 Localizable.xcstrings...\")\n        all_keys = self.extract_all_keys()\n        if not all_keys:\n            return None\n        \n        print(f\"✅ 找到 {len(all_keys)} 个本地化key\")\n        \n        # 查找使用的key\n        used_keys, files_with_keys, missing_in_localization = self.find_all_used_keys(all_keys)\n        \n        # 计算未使用的key\n        unused_keys = all_keys - used_keys\n        missing_keys = used_keys - all_keys  # 这个应该为空，因为used_keys是从all_keys中筛选的\n        \n        result = {\n            'total_keys': len(all_keys),\n            'used_keys': len(used_keys),\n            'unused_keys': len(unused_keys),\n            'missing_keys': len(missing_keys),\n            'missing_in_localization': len(missing_in_localization),\n            'all_keys': sorted(list(all_keys)),\n            'used_keys_list': sorted(list(used_keys)),\n            'unused_keys_list': sorted(list(unused_keys)),\n            'missing_keys_list': sorted(list(missing_keys)),\n            'missing_in_localization_list': sorted(list(missing_in_localization)),\n            'files_scanned': len(self.find_swift_files()),\n            'files_with_keys': files_with_keys\n        }\n        \n        return result\n    \n    def save_results(self, result):\n        \"\"\"保存分析结果到文件\"\"\"\n        # 已移除文件保存功能，只在控制台显示结果\n        pass\n    \n    def print_summary(self, result):\n        \"\"\"打印分析摘要\"\"\"\n        if not result:\n            return\n        \n        print(\"\\n\" + \"=\" * 60)\n        print(\"📊 分析结果摘要\")\n        print(\"=\" * 60)\n        print(f\"总本地化key数量: {result['total_keys']}\")\n        print(f\"使用中的key数量: {result['used_keys']}\")\n        print(f\"未使用的key数量: {result['unused_keys']}\")\n        print(f\"缺失的key数量: {result['missing_keys']} (代码中使用但未定义)\")\n        print(f\"代码中缺失的key: {result['missing_in_localization']} 个\")\n        \n        if result['unused_keys_list']:\n            print(f\"\\n🗑️  未使用的翻译key ({result['unused_keys']} 个):\")\n            for i, key in enumerate(result['unused_keys_list'], 1):\n                print(f\"   {i:2d}. {key}\")\n        \n        if result['missing_keys_list']:\n            print(f\"\\n⚠️  缺失的翻译key ({result['missing_keys']} 个):\")\n            for i, key in enumerate(result['missing_keys_list'], 1):\n                print(f\"   {i:2d}. {key}\")\n        \n        if result['missing_in_localization_list']:\n            print(f\"\\n❌ 代码中使用但未在Localizable.xcstrings中定义的key ({result['missing_in_localization']} 个):\")\n            for i, key in enumerate(result['missing_in_localization_list'], 1):\n                print(f\"   {i:2d}. {key}\")\n        \n        if not result['unused_keys_list'] and not result['missing_keys_list'] and not result['missing_in_localization_list']:\n            print(\"\\n🎉 完美！所有翻译key都被正确使用和定义！\")\n        \n        # 计算使用率\n        usage_rate = (result['used_keys'] / result['total_keys']) * 100 if result['total_keys'] > 0 else 0\n        print(f\"\\n📈 翻译使用率: {usage_rate:.1f}%\")\n\ndef main():\n    project_root = \"/Users/huangfeng/Documents/Bark\"\n    \n    if not os.path.exists(project_root):\n        print(f\"❌ 项目目录不存在: {project_root}\")\n        sys.exit(1)\n    \n    analyzer = BarkLocalizationAnalyzer(project_root)\n    \n    if not analyzer.localization_file.exists():\n        print(f\"❌ 本地化文件不存在: {analyzer.localization_file}\")\n        sys.exit(1)\n    \n    # 执行分析\n    result = analyzer.analyze()\n    \n    if result:\n        # 显示结果\n        analyzer.print_summary(result)\n        \n        print(f\"\\n💡 建议:\")\n        if result['unused_keys'] > 0:\n            print(f\"   - 考虑删除 {result['unused_keys']} 个未使用的翻译key以减小包体积\")\n        if result['missing_keys'] > 0:\n            print(f\"   - 为 {result['missing_keys']} 个缺失的key添加翻译\")\n        if result['missing_in_localization'] > 0:\n            print(f\"   - 为 {result['missing_in_localization']} 个代码中使用的key添加本地化定义\")\n        \n        print(\"   - 检查是否有动态构建的key名称(脚本可能无法检测)\")\n        print(\"   - 手动检查Storyboard/XIB文件中的硬编码字符串\")\n    \n    else:\n        print(\"❌ 分析失败\")\n        sys.exit(1)\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "docs/.nojekyll",
    "content": ""
  },
  {
    "path": "docs/CNAME",
    "content": "bark.day.app"
  },
  {
    "path": "docs/README.md",
    "content": "\n### Bark <!-- {docsify-ignore-all} -->\n- 免费、轻量！简单调用接口即可给自己的iPhone发送推送。\n- 依赖苹果APNs，及时、稳定、可靠\n- 不会消耗设备的电量， 基于系统推送服务与推送扩展，APP本体并不需要运行。\n- 隐私安全，可以通过一些方式确保包含作者本人在内的所有人都无法窃取你的隐私。<br>*点击详细了解如何保障[隐私安全](/privacy)*\n\n### 源码\n- [Bark](https://github.com/Finb/Bark) 是完整开源的 iOS APP，用来接收自定义推送。\n- [bark-server](https://github.com/Finb/bark-server) 是完整开源的 Bark 服务后端，用来接收用户的推送请求并转发给苹果APNS。\n\n### 反馈\n- [Bark 问题反馈群](https://t.me/joinchat/OsCbLzovUAE0YjY1) （注意点击入群验证）\n- [GitHub Issues](https://github.com/Finb/Bark/issues)\n\n### 免费\nBark **2018年7月**上线，至少会维持运营到 **2031年7月** 。*（说不出口“永久”这个词，后续还有需求再续吧）*<br> \nAPP在维持期间，不会有任何形式的收费功能与广告，各位彦祖放心使用。\n\n### 赞助\n目前接收 GitHub 赞助、App内内购赞助。非常感谢每一位赞助者！<br>\n赞助者：[https://github.com/sponsors/Finb](https://github.com/sponsors/Finb)\n\n### 文档\n- **App**\n  - [使用教程](/tutorial)\n  - [推送加密](/encryption)\n  - [常见问题](/faq)\n- **服务端**\n  - [部署服务](/deploy)\n  - [批量推送](/batch)\n  - [编译代码](/build)\n  - [推送证书](/cert)\n- [隐私安全](/privacy)"
  },
  {
    "path": "docs/_coverpage.md",
    "content": "![logo](_media/Icon.png)\n\n# Bark <small></small>\n\n> 一款注重隐私、安全可控的自定义通知推送工具。\n\n- 免费、简单、安全\n- 打开即用\n\n[GitHub](https://github.com/finb/bark)\n[Get Started](#bark)\n"
  },
  {
    "path": "docs/_navbar.md",
    "content": "* Translations\n  - [:cn: 简体中文](/)\n  - [:uk: English](/en-us/)"
  },
  {
    "path": "docs/_sidebar.md",
    "content": "- [Bark](/#bark)\n- **App**\n  - [使用教程](/tutorial)\n  - [推送加密](/encryption)\n  - [常见问题](/faq)\n- **服务端**\n  - [部署服务](/deploy)\n  - [批量推送](/batch)\n  - [编译代码](/build)\n  - [推送证书](/cert)\n- [隐私安全](/privacy)"
  },
  {
    "path": "docs/batch.md",
    "content": "\n### 个人用户\n批量推送仅支持Json请求，需 bark-server 更新至 v2.1.9。（[https://api.day.app](https://api.day.app) 服务器一次最多10个设备， 自建服务器无上限）<br />\n用法:\n```sh\ncurl -X \"POST\" \"https://api.day.app/push\" \\\n     -H 'Content-Type: application/json; charset=utf-8' \\\n     -d $'{\n  \"title\": \"Title\",\n  \"body\": \"Body\",\n  \"sound\": \"minuet\",\n  \"group\": \"test\",\n  \"device_keys\": [\"key1\", \"key2\", ... ]\n}'\n```\n\n### 中间服务\n如果你的服务需要大批量且及时地向用户发送推送，建议自建服务端。可以提供 Url Scheme 方便用户一键更改服务器。\n\nUrl Scheme 示例：\n```\nbark://addServer?address=https%3A%2F%2Fapi.day.app\n```\nbark-server 对配置要求很低，以下是美西 VPS 各配置下的 QPS 测试结果 ：\n\n| Cores | Ram | Speed |\n| ----- | ----------- |----------- |\n| 1 | 3.75 gb |4,023 p/sec |\n| 4 | 16 gb |21,413 p/sec |\n| 16 | 64 gb |64,516 p/sec |\n| 64 | 256 gb |105,263 p/sec |\n\n若 QPS 不高于 200，可继续使用公共服务（[https://api.day.app](https://api.day.app)）。<br />\n若 QPS 超过 200，推荐自建服务端，未来在公共服务器负载过高时，可能会引入流量限制（目前尚未限制）。<br />\n若 QPS 超过 3000，尽量自建服务端，部署时添加 `--max-apns-client-count` 参数，详情请查看[部署文档](/deploy)"
  },
  {
    "path": "docs/build.md",
    "content": "## 下载源码\n从GitHub下载源码 [bark-server](https://github.com/Finb/bark-server)\n\n或\n```sh\ngit clone https://github.com/Finb/bark-server.git\n```\n## 配置依赖\n- Golang 1.18+\n- Go Mod (env GO111MODULE=on)\n- Go Mod Proxy (env GOPROXY=https://goproxy.cn)\n- 安装 [go-task](https://taskfile.dev/installation/) \n\n## 交叉编译所有平台\n```sh\ntask\n```\n\n## 编译指定平台\n```sh\ntask linux_amd64\ntask linux_amd64_v3\n```\n\n## 支持的平台\n\n- linux_386\n- linux_amd64\n- linux_amd64_v2\n- linux_amd64_v3\n- linux_amd64_v4\n- linux_armv5\n- linux_armv6\n- linux_armv7\n- linux_armv8\n- linux_mips_hardfloat\n- linux_mipsle_softfloat\n- linux_mipsle_hardfloat\n- linux_mips64\n- linux_mips64le\n- windows_386.exe\n- windows_amd64.exe\n- windows_amd64_v2.exe\n- windows_amd64_v3.exe\n- windows_amd64_v4.exe\n- darwin_amd64\n- darwin_arm64"
  },
  {
    "path": "docs/cert.md",
    "content": "当你需要集成Bark到自己的系统或重新实现后端代码时可能需要推送证书\n \n##### 有效期到: *永久*\n##### Key ID：*LH4T9V5U4R*\n##### TeamID：*5U8LBRXG3A*\n##### 下载地址：[AuthKey_LH4T9V5U4R_5U8LBRXG3A.p8](https://github.com/Finb/bark-server/releases/download/v1.0.2/AuthKey_LH4T9V5U4R_5U8LBRXG3A.p8)"
  },
  {
    "path": "docs/deploy.md",
    "content": "\n## Docker \n```\ndocker run -dt --name bark -p 8080:8080 -v `pwd`/bark-data:/data finab/bark-server\n```\n> 镜像也可使用 ghcr.io/finb/bark-server\n\n## Docker-Compose \n```\nmkdir bark && cd bark\ncurl -sL https://git.io/JvSRl > docker-compose.yaml\ndocker-compose up -d\n```\n## 手动部署\n\n1. 根据平台下载可执行文件:<br> <a href='https://github.com/Finb/bark-server/releases'>https://github.com/Finb/bark-server/releases</a><br>\n或自己编译<br>\n<a href=\"https://github.com/Finb/bark-server\">https://github.com/Finb/bark-server</a>\n\n2. 运行\n```\n./bark-server_linux_amd64 -addr 0.0.0.0:8080 -data ./bark-data\n```\n3. 你可能需要\n```\nchmod +x bark-server_linux_amd64\n```\n请注意 bark-server 默认使用 /data 目录保存数据，请确保 bark-server 有权限读写 /data 目录，或者你可以使用 `-data` 选项指定一个目录\n\n\n## Cloudflare Workers\n[cwxiaos/bark-worker](https://github.com/cwxiaos/bark-worker)  \n支持 Cloudflare Workers，数据库使用 D1 或 KV  \n> 仅推荐个人用户使用，适合发送少量推送，不适合频繁或大批量推送。  \n\n## 腾讯 EdgeOne / 阿里云 ESA / Cloudflare Workers\n[sylingd/bark-worker-server](https://github.com/sylingd/bark-worker-server)  \n支持腾讯 EdgeOne、阿里云 ESA、Cloudflare Workers，数据库使用 KV  \n> 仅推荐个人用户使用，适合发送少量推送，不适合频繁或大批量推送。\n\n## 宝塔面板\n\n1. 登录宝塔面板，在菜单栏中点击 `Docker`\n\n2. 首次会提示安装`Docker`和`Docker Compose`服务，点击立即安装，若已安装请忽略。\n\n3. 安装完成后在左上角搜索框中搜索`Bark`，点击`安装`。\n\n4. 设置域名等基本信息，点击`确定`\n- 名称：应用名称，默认`bark_随机字符`\n- 版本选择：默认`latest`\n- 域名：如需通过域名直接访问，请在此配置域名并将域名解析到服务器\n- 允许外部访问：如您需通过`IP+Port`直接访问，请勾选，如您已经设置了域名，请不要勾选此处\n- 端口：默认`8080`，可自行修改\n\n5. 提交后面板会自动进行应用初始化，大概需要`1-3`分钟，初始化完成后即可使用。\n\n## 测试\n```\ncurl http://0.0.0.0:8080/ping\n```\n返回 pong 就证明部署成功了\n\n## 大批量推送（普通用户忽略，QPS超过 3000 再使用）\n如果你需要短时间大批量推送，可以配置 bark-server 使用多个 APNS Clients 推送，\n每一个 Client 代表一个新的连接（可能连接到不同的APNs服务器），请根据 CPU 核心数设置这个参数，Client 数量不能超过CPU核心数（超过会自动设置为当前 CPU 核心数）。\n\n配置方法：\n#### Docker\n```\ndocker run -dt --name bark -p 8080:8080 -v `pwd`/bark-data:/data finab/bark-server bark-server --max-apns-client-count 4\n```\n\n#### Docker-Compose \n```yaml\nversion: '3.8'\nservices:\n  bark-server:\n    image: finab/bark-server\n    container_name: bark-server\n    restart: always\n    volumes:\n      - ./data:/data\n    ports:\n      - \"8080:8080\"\n    command: bark-server --max-apns-client-count 4\n```\n\n#### 手动部署\n```\n./bark-server --addr 0.0.0.0:8080 --data ./bark-data --max-apns-client-count 4\n```\n\n\n## 其他\n\n1. APP端负责将<a href=\"https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1622958-application\">DeviceToken</a>发送到服务端。 <br>服务端收到一个推送请求后，将发送推送给Apple服务器。然后手机收到推送\n\n2. 服务端代码: <a href='https://github.com/Finb/bark-server'>https://github.com/Finb/bark-server</a><br>\n\n3. App代码: <a href=\"https://github.com/Finb/Bark\">https://github.com/Finb/Bark</a>\n\n"
  },
  {
    "path": "docs/en-us/README.md",
    "content": "\n### Bark <!-- {docsify-ignore-all} -->\n- Free, lightweight! Simply call the interface to send push notifications to your own iPhone. \n- Depends on Apple APNs, timely, stable and reliable\n- Does not consume device’s battery power. Based on system push service and push extension, APP itself does not need to run. \n- Privacy and security. You can ensure that no one, including the author himself, can steal your privacy through some ways.<br>*Click for details on how to ensure [privacy and security](/en-us/privacy)*\n\n### Source code \n- [Bark](https://github.com/Finb/Bark) is a fully open source iOS APP for receiving custom push notifications\n- [bark-server](https://github.com/Finb/bark-server) is a fully open source Bark service backend for receiving user push requests and forwarding them to Apple APNS.\n\n### Feedback\n- [Telegram Group](https://t.me/joinchat/OsCbLzovUAE0YjY1) （Note: click verification to join group）\n- [GitHub Issues](https://github.com/Finb/Bark/issues)\n\n### Free\nBark was launched in **July 2018** and will maintain operation until at least **July 2031**. *(Can’t say the word “permanent”, let’s renew it later if there is still demand)*<br> \nAPP will not have any form of charge or advertisement during maintenance period. Feel free to use it.\n\n### Sponsorship \nCurrently only accept GitHub sponsorship. Thank you very much for every sponsor<br>\nSponsors：[https://github.com/sponsors/Finb](https://github.com/sponsors/Finb)\n\n### Documentation\n- **App**\n  - [Tutorial](/en-us/tutorial)\n  - [Encryption](/en-us/encryption)  \n  - [FAQs](/en-us/faq)\n- **Server**\n  - [Deploy](/en-us/deploy)\n  - [Batch Push](/en-us/batch)\n  - [Build](/en-us/build)\n  - [Certificate](/en-us/cert)\n- [Privacy](/en-us/privacy)"
  },
  {
    "path": "docs/en-us/_coverpage.md",
    "content": "![logo](../_media/Icon.png)\n\n# Bark <small></small>\n\n> A privacy-focused, secure and controllable custom notification push tool.\n\n- Free, simple and safe\n- Open and go\n\n[GitHub](https://github.com/finb/bark)\n[Get Started](#bark)\n"
  },
  {
    "path": "docs/en-us/_navbar.md",
    "content": "* Translations\n  - [:uk: English](/en-us/)\n  - [:cn: 简体中文](/)"
  },
  {
    "path": "docs/en-us/_sidebar.md",
    "content": "- [Bark](/en-us/#bark)\n- **App**\n  - [Tutorial](/en-us/tutorial)\n  - [Encryption](/en-us/encryption)\n  - [FAQs](/en-us/faq)\n- **Server**\n  - [Deploy](/en-us/deploy)\n  - [Batch Push](/en-us/batch)\n  - [Build](/en-us/build)\n  - [Certificate](/en-us/cert)\n- [Privacy](/en-us/privacy)"
  },
  {
    "path": "docs/en-us/batch.md",
    "content": "\n### Individual Users\nBatch push is only supported with Json requests, and bark-server needs to be updated to v2.1.9. ([https://api.day.app](https://api.day.app) will not be updated to v2.1.9 for now, and currently does not support batch push.)<br />\nUsage:\n```sh\ncurl -X \"POST\" \"https://api.day.app/push\" \\\n     -H 'Content-Type: application/json; charset=utf-8' \\\n     -d $'{\n  \"title\": \"Title\",\n  \"body\": \"Body\",\n  \"sound\": \"minuet\",\n  \"group\": \"test\",\n  \"device_keys\": [\"key1\", \"key2\", ... ]\n}'\n```\n\n### Middleware Services\nIf your service requires sending large volumes of push notifications to users in a timely manner, it is recommended to set up your own server. You can provide a Url Scheme to allow users to change the server with one click.\n\nUrl Scheme Example:\n```\nbark://addServer?address=https%3A%2F%2Fapi.day.app\n```\nbark-server has very low configuration requirements. Below are the QPS test results for various configurations on a US West VPS:\n\n| Cores | Ram | Speed |\n| ----- | ----------- |----------- |\n| 1 | 3.75 gb |4,023 p/sec |\n| 4 | 16 gb |21,413 p/sec |\n| 16 | 64 gb |64,516 p/sec |\n| 64 | 256 gb |105,263 p/sec |\n\nIf QPS does not exceed 200, you can continue to use the public service（[https://api.day.app](https://api.day.app)）。<br />\nIf QPS exceeds 200, it is recommended to set up your own server. In the future, when the public server is under high load, traffic restrictions may be introduced (currently, there are no restrictions).<br />\nIf QPS exceeds 3000, it is strongly recommended to set up your own server and add the --max-apns-client-count parameter during deployment. For details, refer to the[Deployment Documentation.](/en-us/deploy)"
  },
  {
    "path": "docs/en-us/build.md",
    "content": "## Download Source Code\nDownload the source code from GitHub [bark-server](https://github.com/Finb/bark-server)\n\nor\n```sh\ngit clone https://github.com/Finb/bark-server.git\n```\n## Configure Dependencies\n- Golang 1.18+\n- Go Mod (env GO111MODULE=on)\n- Go Mod Proxy (env GOPROXY=https://goproxy.cn)\n- Install [go-task](https://taskfile.dev/installation/) \n\n## Cross-Compile for All Platforms\n```sh\ntask\n```\n\n## Compile for Specific Platforms\n```sh\ntask linux_amd64\ntask linux_amd64_v3\n```\n\n## Supported Platforms\n\n- linux_386\n- linux_amd64\n- linux_amd64_v2\n- linux_amd64_v3\n- linux_amd64_v4\n- linux_armv5\n- linux_armv6\n- linux_armv7\n- linux_armv8\n- linux_mips_hardfloat\n- linux_mipsle_softfloat\n- linux_mipsle_hardfloat\n- linux_mips64\n- linux_mips64le\n- windows_386.exe\n- windows_amd64.exe\n- windows_amd64_v2.exe\n- windows_amd64_v3.exe\n- windows_amd64_v4.exe\n- darwin_amd64\n- darwin_arm64"
  },
  {
    "path": "docs/en-us/cert.md",
    "content": "When you need to integrate Bark into your own system or re-implement the backend code, you may need to push certificates.\n \n##### Valid Until: *Permanent*\n##### Key ID: *LH4T9V5U4R*\n##### Team ID: *5U8LBRXG3A*\n##### Download Link: [AuthKey_LH4T9V5U4R_5U8LBRXG3A.p8](https://github.com/Finb/bark-server/releases/download/v1.0.2/AuthKey_LH4T9V5U4R_5U8LBRXG3A.p8)"
  },
  {
    "path": "docs/en-us/deploy.md",
    "content": "\n## Docker \n```\ndocker run -dt --name bark -p 8080:8080 -v `pwd`/bark-data:/data finab/bark-server\n```\n> You can also use the image ghcr.io/finb/bark-server\n\n## Docker-Compose \n```\nmkdir bark && cd bark\ncurl -sL https://git.io/JvSRl > docker-compose.yaml\ndocker-compose up -d\n```\n## Manual Deployment\n\n1. Download the executable file based on your platform:<br> <a href='https://github.com/Finb/bark-server/releases'>https://github.com/Finb/bark-server/releases</a><br>\nOr compile it yourself:<br>\n<a href=\"https://github.com/Finb/bark-server\">https://github.com/Finb/bark-server</a>\n\n2. Run the server:\n```\n./bark-server_linux_amd64 -addr 0.0.0.0:8080 -data ./bark-data\n```\n3. You may need to make the file executable:\n```\nchmod +x bark-server_linux_amd64\n```\nNote: The bark-server uses the /data directory by default to store data. Ensure it has read/write permissions or specify a custom directory with the -data option.\n\n## Cloudflare Workers\n[cwxiaos/bark-worker](https://github.com/cwxiaos/bark-worker)  \nSupports Cloudflare Workers; databases can be D1 or KV.  \n> Only recommended for personal use. Fine for occasional notifications, but not intended for frequent or large-scale pushes.\n\n## Tencent EdgeOne / Alibaba Cloud ESA / Cloudflare Workers\n[sylingd/bark-worker-server](https://github.com/sylingd/bark-worker-server)  \nSupports Tencent EdgeOne, Alibaba Cloud ESA, and Cloudflare Workers; uses key-value (KV) database.  \n> Only recommended for personal use. Fine for occasional notifications, but not intended for frequent or large-scale pushes.\n\n## Test\n```\ncurl http://0.0.0.0:8080/ping\n```\nIf it returns pong, the deployment is successful.\n\n## High-Volume Push Notifications (For regular users, ignore this. Use only if QPS exceeds 3000)\nIf you need to send a large volume of push notifications in a short period, you can configure the bark-server to use multiple APNS Clients for delivery.\nEach Client represents a new connection (which may connect to different APNs servers). Please set this parameter according to the number of CPU cores. The number of Clients cannot exceed the number of CPU cores (if exceeded, it will automatically be set to the current number of CPU cores).\n\n#### Docker\n```\ndocker run -dt --name bark -p 8080:8080 -v `pwd`/bark-data:/data finab/bark-server bark-server --max-apns-client-count 4\n```\n\n#### Docker-Compose \n```yaml\nversion: '3.8'\nservices:\n  bark-server:\n    image: finab/bark-server\n    container_name: bark-server\n    restart: always\n    volumes:\n      - ./data:/data\n    ports:\n      - \"8080:8080\"\n    command: bark-server --max-apns-client-count 4\n```\n\n#### Manual Deployment\n```\n./bark-server --addr 0.0.0.0:8080 --data ./bark-data --max-apns-client-count 4\n```\n\n\n## 其他\n\n1. The app sends the <a href=\"https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1622958-application\">DeviceToken</a>to the server.<br>The server sends push requests to Apple’s servers.\n2. Server code: <a href='https://github.com/Finb/bark-server'>https://github.com/Finb/bark-server</a><br>\n3. App code: <a href=\"https://github.com/Finb/Bark\">https://github.com/Finb/Bark</a>\n\n"
  },
  {
    "path": "docs/en-us/encryption.md",
    "content": "#### What is push encryption\nPush encryption is a method to protect the push content, which uses a custom key to encrypt and decrypt the push content when sending and receiving. In this way, the push content will not be obtained or leaked by Bark server and Apple APNs server during transmission.\n\n#### Set custom key\n1. Open APP homepage\n2. Find “Push Encryption”, click Encryption Settings \n3. Select encryption algorithm, fill in KEY as required, click Done to save custom key\n\n#### Send encrypted push\nTo send an encrypted push, you need to first convert the Bark request parameters into a json format string, then use the previously set key and corresponding algorithm to encrypt the string, and finally send the encrypted ciphertext as ciphertext parameter to the server.<br><br>\n**For example：**\n```sh\n#!/usr/bin/env bash\n\nset -e\n\n# bark key\ndeviceKey='F5u42Bd3HyW8KxkUqo2gRA'\n# push payload\njson='{\"body\": \"test\", \"sound\": \"birdsong\"}'\n\n# must be 16 bit long\nkey='1234567890123456'\niv='1111111111111111'\n\n# openssl requires Hex encoding of manual keys and IVs, not ASCII encoding.\nkey=$(printf $key | xxd -ps -c 200)\niv=$(printf $iv | xxd -ps -c 200)\n\nciphertext=$(echo -n $json | openssl enc -aes-128-cbc -K $key -iv $iv | base64)\n\n# The console will print \"d3QhjQjP5majvNt5CjsvFWwqqj2gKl96RFj5OO+u6ynTt7lkyigDYNA3abnnCLpr\"\necho $ciphertext\n\ncurl --data-urlencode \"ciphertext=$ciphertext\" http://api.day.app/$deviceKey\n```\n"
  },
  {
    "path": "docs/en-us/faq.md",
    "content": "### Unable to Receive Push Notifications\nCheck whether the Device Token is valid in the app settings. \nIf it’s valid, try rebooting your device. If you still can’t receive notifications, check whether the push request returned HTTP 200.  \n\n### DeviceToken Shows “Unknown”\nThis usually means the device cannot connect to Apple’s servers. You may also notice iMessage not working or other apps not receiving notifications.  \nTry switching networks, rebooting the device, or disabling any proxy/VPN affecting Apple services.  \nThis is a connectivity issue between your device and Apple’s servers, and cannot be fixed by the app author.\n\n### Push Usage Limit\nNormal usage is not restricted. <b>Abnormal usage may result in the IP being banned for 24 hours.</b>  \nIf more than 1,000 TCP connections are established at the same time, new requests will be rejected. When sending a large number of push notifications, please use HTTP/2 to multiplex TCP connections.\n\nBan rules:\n1. More than 1,000 erroneous requests within 5 minutes (HTTP status codes such as 400, 404, 500, etc.).\n2. More than 5 HTTP 405 error requests within 5 minutes\n3. More than 5 erroneous requests within 5 minutes, with the User-Agent being “*Mozilla/5.0 (X11; Linux x86_64)”\n\n> These limits don't apply if you're self-hosting\n\n### Receiving Unknown or Unexpected Pushes (e.g., \"Empty Message\")\nPossible causes:  \n1. Safari may auto-complete the Bark API URL when typing in the address bar and trigger preloading.  \n2. Chat apps like WeChat may periodically access a Bark API URL you sent earlier.  \n3. Your push key was leaked — reset it in the server list page.\n\n### “Server Error” Prompt\nOccasional errors may be ignored. The app might have gone into background causing network timeouts.\n\n### Time-Sensitive Notifications Not Working\nTry **rebooting your device**.\n\n### Unable to Save Notification History or No Copy Button When Pulling Down Notification\nTry **rebooting your device**.  \nThe Notification Service Extension may have failed to run, so the saving logic didn’t execute.\n\n### Multiple Devices Using the Same Key but Only One Receives Notifications\nA key can only be used by one device. Only the most recently opened app instance will receive notifications.\n\n### Auto-Copy Not Working\nOn iOS 14.5+, stricter permissions prevent auto-copy when receiving notifications.  \nYou can instead pull down the notification or swipe left on the lock screen to trigger auto-copy, or tap the copy button.\n\n### Defaulting to Notification History on App Launch\nThe app reopens to the last viewed page.  \nIf you exit the app on the history page, reopening it will return to the history page.\n\n### Does the Push API Support POST Requests?\nBark supports both GET and POST, as well as JSON format.  \nParameters are the same for all request types. See the tutorial for details.\n\n### Push Fails Due to Special Characters (e.g., links, “+” becomes space)\nThis happens when the URL is not properly encoded.\n\n```sh\n# Example\nhttps://api.day.app/key/{content}\n\n# If {content} is:\n\"a/b/c/\"\n\n# Final URL becomes:\nhttps://api.day.app/key/a/b/c/\n# -> No route matches, backend returns 404\n\n# Correct (URL-encoded):\nhttps://api.day.app/key/a%2Fb%2Fc%2F\n```\nHTTP libraries usually encode parameters automatically.\nIf constructing URLs manually, always encode parameters.\n\n#### How to ensure privacy and security\nSee the [Privac](/en-us/privacy)"
  },
  {
    "path": "docs/en-us/privacy.md",
    "content": "#### How Privacy Can Be Leaked <!-- {docsify-ignore-all} -->\nThe route a push notification takes from sending to receiving is as follows:<br>\nSender <font color='red'> → Server①</font> → Apple APNS Server → Your Device → <font color='red'>Bark APP②</font>.\n\nThe two red-marked areas are potential points of privacy leakage <br>\n* The sender does not use HTTPS or uses a public server*（the author can see the request logs）*\n* The Bark App itself is insecure, and the version uploaded to the App Store has been modified.\n\n#### Solving Server-Side Privacy Issues\n* You can use the open-source backend code to [ deploy your own backend service ](/en-us/deploy.md) and enable HTTPS.\n* Use [encrypted push](/en-us/encryption) with a custom key to encrypt the push content.\n\n#### Ensuring the App is Completely Built from Open-Source Code\nTo ensure that the App is secure and has not been modified by anyone (including the author), Bark is built by GitHub Actions and then uploaded to the App Store.<br>\nWithin the Bark app settings, you can view the GitHub Run Id. Clicking on it will allow you to find the configuration files used for the current version's build, the source code at compile time, the build number of the version uploaded to the App Store, and more.<br>\n\n\nThe same build number can only be uploaded to the App Store once, making this number unique.<br>\nYou can use this number to compare with the Bark App downloaded from the store. If they match, it proves that the App downloaded from the App Store is completely built from open-source code.\n\nExample: Bark 1.2.9 - 3 <br> \nhttps://github.com/Finb/Bark/actions/runs/3327969456\n\n1. Find the commit id at compile time to view the complete source code at compile time.\n2. Check .github/workflows/testflight.yaml to verify all Actions and ensure that the logs printed by the Actions have not been tampered with.\n3. View Action Logs https://github.com/Finb/Bark/actions/runs/3327969456/jobs/5503414528\n4. Find the packaged App ID, Team ID, version, and build number uploaded to the App Store, among other information.\n5. Download the corresponding version ipa from the store and compare whether the build number matches the one in the logs*（this number is unique for the same APP, and once successfully uploaded, it cannot be uploaded again with the same version build number）*\n\n\n*Here, we do not consider whether iOS leaks privacy.*"
  },
  {
    "path": "docs/en-us/tutorial.md",
    "content": "## Sending Push Notifications\n1. Open the APP and copy the test URL. \n\n<img src=\"../_media/example.jpg\" width=365 />\n\n2. Modify the content and request this URL.<br>\nYou can send a GET or POST request. If the request is successful, you will receive the push notification immediately.\n\n## URL Format\nThe URL consists of the push key, parameter title, parameter subtitle, and parameter body. There are three combinations:\n\n```\n/:key/:body \n/:key/:title/:body \n/:key/:title/:subtitle/:body \n```\n\n## Request Methods\n##### GET request parameters are appended to the URL, for example:\n```sh\ncurl https://api.day.app/your_key/body?group=groupName&copy=copyText\n```\n*When manually appending parameters to the URL, please pay attention to URL encoding issues.*\n\n##### POST request parameters are placed in the request body, for example:\n```sh\ncurl -X POST https://api.day.app/your_key \\\n     -d'body=body&group=groupName&copy=copyText'\n```\n##### POST requests support JSON, for example:\n```sh\ncurl -X \"POST\" \"https://api.day.app/your_key\" \\\n     -H 'Content-Type: application/json; charset=utf-8' \\\n     -d $'{\n  \"body\": \"Test Body\",\n  \"title\": \"Test Title\",\n  \"badge\": 1,\n  \"sound\": \"minuet\",\n  \"icon\": \"https://day.app/assets/images/avatar.jpg\",\n  \"group\": \"test\",\n  \"url\": \"https://mritd.com\"\n}'\n```\n\n##### JSON request key can be placed in the request body, the URL path must be /push, for example:\n```sh\ncurl -X \"POST\" \"https://api.day.app/push\" \\\n     -H 'Content-Type: application/json; charset=utf-8' \\\n     -d '{\n  \"body\": \"Test Body\",\n  \"title\": \"Test Title\",\n  \"device_key\": \"your_key\"\n}'\n```\n\n#### MCP\nVS Code:  \n```js\n{\n  \"servers\": {\n    \"bark\": {\n      \"type\": \"http\",\n      \"url\": \"https://api.day.app/mcp/{key}\"\n    }\n  }\n}\n```\n\nClaude Code:   \n```sh\nclaude mcp add bark --transport http https://api.day.app/mcp/{key}\n```  \nor  \n```js\n{\n  \"mcpServers\": {\n    \"bark\": {\n      \"type\": \"http\",\n      \"url\": \"https://api.day.app/mcp/{key}\"\n    }\n  }\n}\n```  \n> Note: Replace {key} in the URL with your own key.\n\n\n## Request Parameters\nList of supported parameters, specific effects can be previewed in the APP.\n\n| Parameter | Description |\n| ----- | ----------- |\n| title | Push title |\n| subtitle | Push subtitle |\n| body | Push content |\n| markdown | Push content with basic Markdown. When this is provided, the body field is ignored. | \n| device_key | Device key |\n| device_keys | Key array, used for batch push. Only supported in JSON requests. |\n| level | Push interruption level.<br>critical: Important alert, will ring even in silent mode <br>active：Default value, the system will immediately light up the screen to display the notification<br>timeSensitive：Time-sensitive notification, can display the notification in focus mode.<br>passive：Only adds the notification to the notification list, will not light up the screen. |\n| volume | Important alert notification volume, value range: 0-10, default is 5 if not passed |\n| badge | Push badge, can be any number |\n| call | Pass \"1\" to repeat the notification ringtone |\n| autoCopy | Pass \"1\" to automatically copy the push content on iOS14.5 and below, on iOS14.5 and above, you need to long press or pull down the push to copy |\n| copy | When copying the push, specify the content to copy, if this parameter is not passed, the entire push content will be copied. |\n| sound | Can set different ringtones for the push |\n| icon | Set a custom icon for the push, the set icon will replace the default Bark icon. <br>The icon will be automatically cached on the device, the same icon URL will only be downloaded once. |\n| image | Push image URL |\n| group | Group messages, pushes will be displayed in the notification center by group.<br>You can also choose to view different groups in the history message list. |\n| ciphertext | Ciphertext for encrypted push |\n| isArchive | Pass 1 to save the push, pass other values to not save the push, if not passed, it will be decided by the APP settings whether to save. |\n| url | URL to jump to when the push is clicked, supports URL Scheme and Universal Link |\n| action | When set to “alert”, tapping the push notification and opening the app will display an action popup. |\n| id | When using the same ID value, it will update the corresponding push notification content<br>Requires Bark v1.5.2, bark-server v2.2.5 or above |\n| delete | Pass \"1\" to delete the notification from the system notification center and APP history, must be used with the id parameter<br>Requires \"Background App Refresh\" to be enabled in settings, otherwise it will not work. |\n\n## Bark 支持的应用程序和插件\n* [SmsForwarder](https://github.com/pppscn/SmsForwarder) Monitors SMS, calls, and app notifications on Android devices and forwards them to Bark based on rules.\n* [acme.sh](https://github.com/acmesh-official/acme.sh/wiki/notify#16-set-notification-for-ios-bark) Generates free certificates from ZeroSSL, Let’s Encrypt, and other CAs; Bark can be used to receive acme.sh cronjob notifications.\n* [Uptime-Kuma](https://github.com/louislam/uptime-kuma) A self-hosted monitoring tool that supports Bark as an alert channel.\n* [Apprise](https://github.com/caronc/apprise) Sends notifications to almost all platforms and supports Bark.\n* [浏览器扩展](https://github.com/ij369/bark-sender) Sends webpage content to your phone.\n* [RevenueBell](https://github.com/woxiqingxian/RevenueBell) A tool for indie developers that pushes Apple subscription, renewal, and purchase revenue events to your phone via Bark.\n\n## Shortcuts\nBark supports sending notifications directly via Shortcuts."
  },
  {
    "path": "docs/encryption.md",
    "content": "#### 什么是推送加密\n推送加密是一种保护推送内容的方法，它使用自定义秘钥在发送和接收时对推送内容进行加密和解密。<br>这样，推送内容在传输过程中就不会被 Bark 服务器和苹果 APNs 服务器获取或泄露。\n\n#### 设置自定义秘钥\n1. 打开APP首页\n2. 找到 “推送加密” ，点击加密设置\n3. 选择加密算法，按要求填写KEY，点击完成保存自定义秘钥\n\n#### 发送加密推送\n要发送加密推送，首先需要把 Bark 请求参数转换成 json 格式的字符串，然后用之前设置的秘钥和相应的算法对字符串进行加密，最后把加密后的密文作为ciphertext参数发送到服务器。<br><br>\n**示例：**\n```sh\n#!/usr/bin/env bash\n\nset -e\n\n# bark key\ndeviceKey='F5u42Bd3HyW8KxkUqo2gRA'\n# push payload\njson='{\"body\": \"test\", \"sound\": \"birdsong\"}'\n\n# Must be 16 bit long\nkey='1234567890123456'\n# IV can be randomly generated, but if it is random, it needs to be passed in the iv parameter.\niv='1234567890123456'\n\n# openssl requires Hex encoding of manual keys and IVs, not ASCII encoding.\nkey=$(printf $key | xxd -ps -c 200)\niv=$(printf $iv | xxd -ps -c 200)\n\nciphertext=$(echo -n $json | openssl enc -aes-128-cbc -K $key -iv $iv | base64)\n\n# The console will print \"+aPt5cwN9GbTLLSFri60l3h1X00u/9j1FENfWiTxhNHVLGU+XoJ15JJG5W/d/yf0\"\necho $ciphertext\n\n# URL encoding the ciphertext, there may be special characters.\ncurl --data-urlencode \"ciphertext=$ciphertext\" --data-urlencode \"iv=1234567890123456\" https://api.day.app/$deviceKey\n```"
  },
  {
    "path": "docs/faq.md",
    "content": "#### 无法收到推送\n在 App 设置中检查 Device Token 是否正常。如果不正常，参考 [这里](#DeviceToken显示未知)<br/>\n如果正常，可以重启下设备，如果还不能接收到推送，检查推送请求返回状态码是否为 code 200。<br/>\n排查都正常后还有问题可在[Bark 问题反馈群](https://t.me/joinchat/OsCbLzovUAE0YjY1)反馈。\n\n#### DeviceToken显示未知\n这很有可能是设备没有正常连接到苹果服务器，伴随发生 iMessage 不可用、其他 App 推送也收不到等问题。<br/>\n可以尝试切换网络、重启手机、如果翻墙代理了 Apple 服务可以关闭翻墙工具。<br/>\n此问题是用户设备与苹果服务器的连接问题，作者并不能提供任何帮助，需自己尝试解决。\n\n#### 推送使用次数限制\n正常使用无任何限制，异常使用<b> IP 会被 BAN 24小时</b>  \n同一时刻建立了超过 1000 条 TCP 链接，新增的请求将被拒绝，发送大量推送时，请使用 HTTP/2 复用 TCP 链接。\n\n被BAN规则：\n1. 5分钟内超过 1000 次错误请求（HTTP状态码为 400 404 500 等）\n2. 5分钟内超过 5 次 405 错误请求（主要是防止某未知软件，会大量 POST api.day.app)\n3. 5分钟内超过 5 次错误请求，并且 User-Agent 为 “*Mozilla/5.0 (X11; Linux x86_64)” （疑似绿邮会大量重试未发送成功的短信）\n\n> 自行部署的 Bark-Server 无以上限制\n\n#### 莫名收到未知推送，比如 \"Empty Message\"\n可能的原因：<br>\n1. 如果用 Safari 发送过推送，在Safari输入任意网址时，可能 Safari 对历史记录搜索进行自动补全时，正好补全成 Bark API 的 URL，然后预加载触发推送。\n2. 如果将 Bark API URL 发送到聊天软件如微信文件传输助手，微信会不定时的请求 URL 触发推送。\n3. 推送 Key 泄露，推荐在服务器列表页面重置 Key。\n\n#### 提示服务器错误\n偶尔提示一次可以忽略，可能是设备进入后台导致网络请求超时之类的原因导致的。<br>\n\n#### 时效性通知无效 \n可以尝试<b>重启设备</b>来解决。\n\n#### 无法保存通知历史，或下拉推送没有点击复制按钮无法复制\n可以尝试<b>重启设备</b>来解决。<br />\n因某些原因导致推送服务扩展（[UNNotificationServiceExtension](https://developer.apple.com/documentation/usernotifications/unnotificationserviceextension)）未能正常运行，执行通知保存的代码未能正常执行。\n\n#### 多台设备使用同一个key，但只有其中一台设备可以收到推送\n同一个Key只能一台设备使用，只有最后打开的APP会收到推送\n\n#### 自动复制推送失效\niOS 14.5 之后的版本因权限收紧，不能在收到推送时自动复制推送内容到剪切板。<br/>\n可暂时先下拉推送或在锁屏界面左滑推送点查看即可自动复制，或点击弹出的推送复制按钮。\n\n#### 默认打开通知历史列表\n再次开启APP时，会跳转到上次打开的页面。<br />\n只需退出APP时，停留在历史消息页面，再次打开APP时就是历史消息页面。\n\n#### 推送 API 是否支持 POST 请求？\nBark支持 GET POST ,支持使用Json<br>\n无论哪种请求方式，参数名都一样, 参考[使用教程](/tutorial#请求方式)\n\n#### 推送特殊字符导致推送失败，比如 推送内容包含链接，或推送异常 比如 + 变成空格\n这是因为整个链接不规范导致的问题，常发生在自己手动拼接URL时。<br>\n拼接URL时，注意将参数进行URL编码 \n\n```sh\n# 例如\nhttps://api.day.app/key/{推送内容}\n\n# 如果{推送内容}是\n\"a/b/c/\"\n\n# 则最后拼接的URL是\nhttps://api.day.app/key/a/b/c/\n# 将找不到对应的路由，后端程序将返回404\n\n# 应该将 {推送内容} url编码后再进行拼接\nhttps://api.day.app/key/a%2Fb%2Fc%2F\n```\n如果是使用成熟的HTTP库时，参数都会被自动处理，无需自己手动编码。<br>\n但如果是自己去拼接URL时，则需要特别注意参数中的特殊字符，**最好不管有没有特殊字符，无脑套一层URL编码**。\n\n#### 如何保障隐私安全\n参考[隐私安全](/privacy)\n"
  },
  {
    "path": "docs/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n  <meta charset=\"UTF-8\">\n  <title>Bark</title>\n  <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge,chrome=1\" />\n  <meta name=\"description\" content=\"Description\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, minimum-scale=1.0\">\n  <link rel=\"stylesheet\" href=\"//cdn.jsdelivr.net/npm/docsify@4/lib/themes/vue.css\">\n</head>\n\n<body>\n  <div id=\"app\"></div>\n  <script>\n    window.$docsify = {\n      name: 'Bark',\n      nameLink: '/#/en-us/?id=source-code',\n      // repo: 'https://github.com/Finb/Bark',\n      logo: '/_media/Icon.png height=60px',\n      coverpage: true,\n      loadSidebar: true,\n      subMaxLevel: 4,\n      loadNavbar: true,\n      auto2top: true,\n      coverpage: ['/', '/en-us/'],\n    }\n  </script>\n  <!-- Docsify v4 -->\n  <script src=\"//cdn.jsdelivr.net/npm/docsify@4\"></script>\n  <script src=\"//cdn.jsdelivr.net/npm/prismjs@1/components/prism-bash.min.js\"></script>\n</body>\n\n<!-- Cloudflare Web Analytics -->\n<script defer src='https://static.cloudflareinsights.com/beacon.min.js'\n  data-cf-beacon='{\"token\": \"6ad048c5cd404e309d826a089e2826f1\"}'></script>\n<!-- End Cloudflare Web Analytics -->\n\n</html>"
  },
  {
    "path": "docs/privacy.md",
    "content": "#### 隐私如何泄露 <!-- {docsify-ignore-all} -->\n一条推送从发送到接收经过路线是：<br>\n发送端 <font color='red'> →服务端①</font> → 苹果APNS服务器 → 你的设备 → <font color='red'>Bark APP②</font>。\n\n红色的两处地方可能泄露隐私 <br>\n* 发送端未使用HTTPS或使用公共服务器*（作者会看到请求日志）*\n* Bark App 本身不安全，上传到 App Store 的版本经过修改。\n\n#### 解决服务端隐私问题\n* 你可以使用开源的后端代码，自行[部署后端服务](/deploy.md)，开启HTTPS。\n* 使用自定义秘钥的[加密推送](/encryption) ，加密推送内容\n\n#### 保证 APP 完全由开源代码构建\n为确保 App 是安全、未经任何人（包含作者）修改过的，Bark 是由 GitHub Actions 构建后上传到 App Store。<br>\nBark应用设置内可以查看到 GitHub Run Id，点击可在里面找到当前版本构建所使用的配置文件、编译时的源代码、上传到 App Store 的版本 build 号 等等信息。<br>\n\n\n同一个版本 build 号仅能上传到 App Store 一次，所以这个号是唯一的。<br>\n可用此号对比从商店下载的 Bark App，如果一致则证明从 App Store 下载的 App 是完全由开源代码构建。\n\n举例： Bark 1.2.9 - 3 <br> \nhttps://github.com/Finb/Bark/actions/runs/3327969456\n\n1. 找到编译时的 commit id ，可以查看编译时完整的源码\n2. 查看 .github/workflows/testflight.yaml ，验证所有 Action ，确保 Action 打印的日志未被篡改\n3. 查看 Action Logs https://github.com/Finb/Bark/actions/runs/3327969456/jobs/5503414528\n4. 找到 打包的App ID、Team ID、上传到 App Store 的版本与 build 号等信息。\n5. 下载商店对应版本ipa，比对版本build号是否与日志中一致*（这个号码同一个APP是唯一的，成功上传了就不能再以相同的版本build号上传）*\n\n\n*这里不考虑iOS是否泄露隐私*"
  },
  {
    "path": "docs/tutorial.md",
    "content": "## 发送推送\n1. 打开APP，复制测试URL \n\n<img src=\"../_media/example.jpg\" width=365 />\n\n2. 修改内容，请求这个URL。<br>\n可以发 GET 或者 POST 请求 ，请求成功会立即收到推送 \n\n## URL格式\nURL由推送key、参数 title、参数 subtitle、参数 body 组成。有下面三种组合方式\n\n```\n/:key/:body \n/:key/:title/:body \n/:key/:title/:subtitle/:body \n```\n\n## 请求方式\n##### GET 请求参数拼接在 URL 后面，例如：\n```sh\ncurl https://api.day.app/your_key/推送内容?group=分组&copy=复制\n```\n*手动拼接参数到URL上时，请注意URL编码问题，可以参考阅读[常见问题：URL编码](/faq?id=%e6%8e%a8%e9%80%81%e7%89%b9%e6%ae%8a%e5%ad%97%e7%ac%a6%e5%af%bc%e8%87%b4%e6%8e%a8%e9%80%81%e5%a4%b1%e8%b4%a5%ef%bc%8c%e6%af%94%e5%a6%82-%e6%8e%a8%e9%80%81%e5%86%85%e5%ae%b9%e5%8c%85%e5%90%ab%e9%93%be%e6%8e%a5%ef%bc%8c%e6%88%96%e6%8e%a8%e9%80%81%e5%bc%82%e5%b8%b8-%e6%af%94%e5%a6%82-%e5%8f%98%e6%88%90%e7%a9%ba%e6%a0%bc)*\n\n##### POST 请求参数放在请求体中，例如：\n```sh\ncurl -X POST https://api.day.app/your_key \\\n     -d'body=推送内容&group=分组&copy=复制'\n```\n##### POST 请求支持JSON，例如：\n```sh\ncurl -X \"POST\" \"https://api.day.app/your_key\" \\\n     -H 'Content-Type: application/json; charset=utf-8' \\\n     -d $'{\n  \"body\": \"Test Body\",\n  \"title\": \"Test Title\",\n  \"badge\": 1,\n  \"sound\": \"minuet\",\n  \"icon\": \"https://day.app/assets/images/avatar.jpg\",\n  \"group\": \"test\",\n  \"url\": \"https://bark.day.app\"\n}'\n```\n\n##### JSON 请求 key 可以放进请求体中,URL 路径须为 /push，例如\n```sh\ncurl -X \"POST\" \"https://api.day.app/push\" \\\n     -H 'Content-Type: application/json; charset=utf-8' \\\n     -d '{\n  \"body\": \"Test Body\",\n  \"title\": \"Test Title\",\n  \"device_key\": \"your_key\"\n}'\n```\n\n#### MCP\nVS Code:  \n```js\n{\n  \"servers\": {\n    \"bark\": {\n      \"type\": \"http\",\n      \"url\": \"https://api.day.app/mcp/{key}\"\n    }\n  }\n}\n```\n\nClaude Code:   \n```sh\nclaude mcp add bark --transport http https://api.day.app/mcp/{key}\n```  \n或者  \n```js\n{\n  \"mcpServers\": {\n    \"bark\": {\n      \"type\": \"http\",\n      \"url\": \"https://api.day.app/mcp/{key}\"\n    }\n  }\n}\n```  \n> 注意将 url 中的 key 替换成你自己的\n\n\n## 请求参数\n支持的参数列表，具体效果可在APP内预览。\n\n| 参数 | 说明 |\n| ----- | ----------- |\n| title | 推送标题 |\n| subtitle | 推送副标题 |\n| body | 推送内容 |\n| markdown | 推送内容，支持基础 Markdown 格式。传递了此参数将忽略 body 字段, 发送时请注意处理特殊字符。| \n| device_key | 设备key |\n| device_keys | key 数组，用于批量推送，仅支持 Json 请求使用。|\n| level | 推送中断级别。<br>critical: 重要警告, 在静音模式下也会响铃 <br>active：默认值，系统会立即亮屏显示通知<br>timeSensitive：时效性通知，可在专注状态下显示通知。<br>passive：仅将通知添加到通知列表，不会亮屏提醒。 |\n| volume | 重要警告的通知音量，取值范围：0-10，不传默认值为5 |\n| badge | 推送角标，可以是任意数字 |\n| call | 传\"1\"时，通知铃声重复播放 |\n| autoCopy | 传\"1\"时， iOS14.5以下自动复制推送内容，iOS14.5以上需手动长按推送或下拉推送 |\n| copy | 复制推送时，指定复制的内容，不传此参数将复制整个推送内容。 |\n| sound | 可以为推送设置不同的铃声 |\n| icon | 为推送设置自定义图标，设置的图标将替换默认Bark图标。<br>图标会自动缓存在本机，相同的图标 URL 仅下载一次。 |\n| image | 推送图片 url |\n| group | 对消息进行分组，推送将按group分组显示在通知中心中。<br>也可在历史消息列表中选择查看不同的群组。 |\n| ciphertext | 加密推送的密文 |\n| isArchive | 传 1 保存推送，传其他的不保存推送，不传按APP内设置来决定是否保存。 |\n| url | 点击推送时，跳转的URL ，支持URL Scheme 和 Universal Link |\n| action | 传 \"alert\" 时，点击推送跳转到APP时会弹出操作弹窗 |\n| id | 使用相同的ID值时，将更新对应推送的通知内容<br>需 Bark v1.5.2, bark-server v2.2.5 以上，Json传参需使用字符串类型 |\n| delete | 传 \"1\" 时，将从系统通知中心和APP内历史记录中删除通知，需搭配 id 参数使用<br>需在设置里开启”后台App刷新“，否则无效。|\n\n## Bark 支持的应用程序和插件\n* [SmsForwarder](https://github.com/pppscn/SmsForwarder) 监控 Android 手机短信、来电、APP通知，并根据指定规则转发到Bark。\n* [acme.sh](https://github.com/acmesh-official/acme.sh/wiki/notify#16-set-notification-for-ios-bark) 从 ZeroSSL，Let's Encrypt 等 CA 生成免费的证书。可以使用 Bark 接收 acme.sh cronjob 任务通知。\n* [Uptime-Kuma](https://github.com/louislam/uptime-kuma) 自托管监控工具, 支持Bark作为告警通道。\n* [Apprise](https://github.com/caronc/apprise) 可以给几乎所有平台发送通知，支持Bark。\n* [浏览器扩展](https://github.com/ij369/bark-sender) 将网页内容发送到手机\n* [RevenueBell](https://github.com/woxiqingxian/RevenueBell) 独立开发者工具，将苹果订阅、续订、购买等收入事件，通过 Bark 推送到你的手机。\n\n## 快捷指令\nBark 支持使用快捷指令直接发送推送"
  },
  {
    "path": "fastlane/Appfile",
    "content": "app_identifier(\"me.fin.bark\") # The bundle identifier of your app\napple_id(\"finuuid@gmail.com\") # Your Apple email address\n\nitc_team_id(\"118137723\") # App Store Connect Team ID\nteam_id(\"5U8LBRXG3A\") # Developer Portal Team ID\n\n# For more information about the Appfile, see:\n#     https://docs.fastlane.tools/advanced/#appfile\n"
  },
  {
    "path": "fastlane/Fastfile",
    "content": "# This file contains the fastlane.tools configuration\n# You can find the documentation at https://docs.fastlane.tools\n#\n# For a list of all available actions, check out\n#\n#     https://docs.fastlane.tools/actions\n#\n# For a list of all available plugins, check out\n#\n#     https://docs.fastlane.tools/plugins/available-plugins\n#\n\n# Uncomment the line if you want fastlane to automatically update itself\n# update_fastlane\n\ndefault_platform(:ios)\n\nplatform :ios do\n  desc \"Push a new beta build to TestFlight\"\n  lane :beta do | options |\n    setup_ci\n    \n    version = options[:store_version_number]\n    if !version.nil? && !version.empty?\n      increment_version_number(version_number: version)\n    end\n\n    build = options[:build_number]\n    if !build.nil? && !build.empty?\n      increment_build_number(build_number: build.to_i)\n    end\n    \n    run_id = ENV[\"GITHUB_RUN_ID\"]\n    if !run_id.nil? && !run_id.empty?\n      set_info_plist_value(path: \"./Bark/Info.plist\", key: \"GitHub Run Id\", value: run_id)\n    end\n    \n    match(type: \"appstore\", readonly: is_ci)\n\n    build_app(workspace: \"Bark.xcworkspace\", scheme: \"Bark\")\n\n    app_store_connect_api_key(\n      key_id: \"DX95H785DP\",\n      issuer_id: ENV[\"APP_STORE_CONNECT_ISSUER_ID\"],\n      key_content: ENV[\"APP_STORE_CONNECT_KEY_CONTENT\"],\n      duration: 1200, # optional (maximum 1200)\n      in_house: false # optional but may be required if using match/sigh\n    )    \n\n    upload_to_testflight\n\n    bark_key = ENV[\"BARK_KEY\"]\n    %x( 'curl' 'https://api.day.app/#{bark_key}/Bark%20#{version}%20(#{build})%20has%20completed%20processing?group=Github%20Actions&url=https%3A%2F%2Fgithub.com%2FFinb%2FBark%2Factions%2Fruns%2F#{run_id}' )\n    \n  end\n\n  lane :tests do\n    run_tests(scheme: \"Bark.xcworkspace\", scheme: \"Bark\")\n  end\nend\n"
  },
  {
    "path": "fastlane/Matchfile",
    "content": "git_url(\"https://github.com/Finb/match.git\")\n\nstorage_mode(\"git\")\n\ntype(\"appstore\") # The default type, can be: appstore, adhoc, enterprise or development\n\napp_identifier([\"me.fin.bark\", \"me.fin.bark.NotificationContent\", \"me.fin.bark.NotificationServiceExtension\", \"me.fin.bark.Intent\"])\n# username(\"user@fastlane.tools\") # Your Apple Developer Portal username\n\n# For all available options run `fastlane match --help`\n# Remove the # in the beginning of the line to enable the other options\n\n# The docs are available on https://docs.fastlane.tools/actions/match\n"
  },
  {
    "path": "notificationContentExtension/Base.lproj/MainInterface.storyboard",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<document type=\"com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB\" version=\"3.0\" toolsVersion=\"14269.12\" targetRuntime=\"iOS.CocoaTouch\" propertyAccessControl=\"none\" useAutolayout=\"YES\" useTraitCollections=\"YES\" useSafeAreas=\"YES\" colorMatched=\"YES\" initialViewController=\"M4Y-Lb-cyx\">\n    <device id=\"retina4_7\" orientation=\"portrait\">\n        <adaptation id=\"fullscreen\"/>\n    </device>\n    <dependencies>\n        <deployment identifier=\"iOS\"/>\n        <plugIn identifier=\"com.apple.InterfaceBuilder.IBCocoaTouchPlugin\" version=\"14252.5\"/>\n        <capability name=\"Safe area layout guides\" minToolsVersion=\"9.0\"/>\n        <capability name=\"documents saved in the Xcode 8 format\" minToolsVersion=\"8.0\"/>\n    </dependencies>\n    <scenes>\n        <!--Notification View Controller-->\n        <scene sceneID=\"cwh-vc-ff4\">\n            <objects>\n                <viewController id=\"M4Y-Lb-cyx\" userLabel=\"Notification View Controller\" customClass=\"NotificationViewController\" customModule=\"NotificationContentExtension\" customModuleProvider=\"target\" sceneMemberID=\"viewController\">\n                    <view key=\"view\" contentMode=\"scaleToFill\" simulatedAppContext=\"notificationCenter\" id=\"S3S-Oj-5AN\">\n                        <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"320\" height=\"37\"/>\n                        <autoresizingMask key=\"autoresizingMask\" widthSizable=\"YES\" heightSizable=\"YES\"/>\n                        <color key=\"backgroundColor\" white=\"0.0\" alpha=\"0.0\" colorSpace=\"custom\" customColorSpace=\"genericGamma22GrayColorSpace\"/>\n                        <viewLayoutGuide key=\"safeArea\" id=\"2BE-c3-nQJ\"/>\n                    </view>\n                    <extendedEdge key=\"edgesForExtendedLayout\"/>\n                    <freeformSimulatedSizeMetrics key=\"simulatedDestinationMetrics\"/>\n                    <size key=\"freeformSize\" width=\"320\" height=\"37\"/>\n                </viewController>\n                <placeholder placeholderIdentifier=\"IBFirstResponder\" id=\"vXp-U4-Rya\" userLabel=\"First Responder\" sceneMemberID=\"firstResponder\"/>\n            </objects>\n        </scene>\n    </scenes>\n</document>\n"
  },
  {
    "path": "notificationContentExtension/Info.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>CFBundleDevelopmentRegion</key>\n\t<string>$(DEVELOPMENT_LANGUAGE)</string>\n\t<key>CFBundleDisplayName</key>\n\t<string>NotificationContentExtension</string>\n\t<key>CFBundleExecutable</key>\n\t<string>$(EXECUTABLE_NAME)</string>\n\t<key>CFBundleIdentifier</key>\n\t<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>\n\t<key>CFBundleInfoDictionaryVersion</key>\n\t<string>6.0</string>\n\t<key>CFBundleName</key>\n\t<string>$(PRODUCT_NAME)</string>\n\t<key>CFBundlePackageType</key>\n\t<string>XPC!</string>\n\t<key>CFBundleShortVersionString</key>\n\t<string>$(MARKETING_VERSION)</string>\n\t<key>CFBundleVersion</key>\n\t<string>$(CURRENT_PROJECT_VERSION)</string>\n\t<key>NSExtension</key>\n\t<dict>\n\t\t<key>NSExtensionAttributes</key>\n\t\t<dict>\n\t\t\t<key>UNNotificationExtensionCategory</key>\n\t\t\t<array>\n\t\t\t\t<string>copy</string>\n\t\t\t\t<string>myNotificationCategory</string>\n\t\t\t</array>\n\t\t\t<key>UNNotificationExtensionInitialContentSizeRatio</key>\n\t\t\t<real>0.01</real>\n\t\t</dict>\n\t\t<key>NSExtensionMainStoryboard</key>\n\t\t<string>MainInterface</string>\n\t\t<key>NSExtensionPointIdentifier</key>\n\t\t<string>com.apple.usernotifications.content-extension</string>\n\t</dict>\n</dict>\n</plist>\n"
  },
  {
    "path": "notificationContentExtension/NotificationContentExtension.entitlements",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>com.apple.security.application-groups</key>\n\t<array>\n\t\t<string>group.bark</string>\n\t</array>\n</dict>\n</plist>\n"
  },
  {
    "path": "notificationContentExtension/NotificationViewController.swift",
    "content": "//\n//  NotificationViewController.swift\n//  NotificationContentExtension\n//\n//  Created by huangfeng on 2018/7/4.\n//  Copyright © 2018 Fin. All rights reserved.\n//\n\nimport UIKit\nimport UserNotifications\nimport UserNotificationsUI\n\nclass NotificationViewController: UIViewController, UNNotificationContentExtension {\n    let noticeLabel: UILabel = {\n        let label = UILabel()\n        label.textColor = UIColor(named: \"notification_copy_color\")\n        label.font = UIFont.preferredFont(ofSize: 16)\n        label.adjustsFontForContentSizeCategory = true\n        label.textAlignment = .center\n        return label\n    }()\n\n    /// 增加图片view\n    let imageView: UIImageView = {\n        let view = UIImageView()\n        view.contentMode = .scaleAspectFit\n        return view\n    }()\n\n    override func viewDidLoad() {\n        super.viewDidLoad()\n        self.view.addSubview(self.noticeLabel)\n        self.view.addSubview(self.imageView)\n        self.preferredContentSize = CGSize(width: 0, height: 1)\n    }\n\n    override func viewWillAppear(_ animated: Bool) {\n        super.viewWillAppear(animated)\n        self.preferredContentSize = CGSize(width: 0, height: 1)\n    }\n\n    override func viewDidAppear(_ animated: Bool) {\n        super.viewDidAppear(animated)\n        self.preferredContentSize = CGSize(width: 0, height: 1)\n    }\n\n    func didReceive(_ notification: UNNotification) {\n        ///  处理图片显示\n        self.ImageHandler(notification)\n\n        guard notification.request.content.userInfo[\"autocopy\"] as? String == \"1\"\n            || notification.request.content.userInfo[\"automaticallycopy\"] as? String == \"1\"\n        else {\n            return\n        }\n        if let copy = notification.request.content.userInfo[\"copy\"] as? String {\n            UIPasteboard.general.string = copy\n        } else {\n            UIPasteboard.general.string = notification.request.content.bodyText\n        }\n    }\n\n    func didReceive(_ response: UNNotificationResponse, completionHandler completion: @escaping (UNNotificationContentExtensionResponseOption) -> Void) {\n        switch response.actionIdentifier {\n        case \"copy\":\n            self.copyAction(response, completionHandler: completion)\n        case \"mute\":\n            self.muteAction(response, completionHandler: completion)\n        default:\n            completion(.dismiss)\n        }\n    }\n\n    /// 复制\n    func copyAction(_ response: UNNotificationResponse, completionHandler completion: @escaping (UNNotificationContentExtensionResponseOption) -> Void) {\n        let userInfo = response.notification.request.content.userInfo\n\n        if let copy = userInfo[\"copy\"] as? String {\n            UIPasteboard.general.string = copy\n        } else {\n            var content = \"\"\n            if !response.notification.request.content.title.isEmpty {\n                content += \"\\(response.notification.request.content.title)\\n\"\n            }\n            if !response.notification.request.content.subtitle.isEmpty {\n                content += \"\\(response.notification.request.content.subtitle)\\n\"\n            }\n            if !response.notification.request.content.bodyText.isEmpty {\n                content += \"\\(response.notification.request.content.bodyText)\\n\"\n            }\n            if let url = userInfo[\"url\"] as? String, !url.isEmpty {\n                content += \"\\(url)\\n\"\n            }\n            content = content.trimmingCharacters(in: .whitespacesAndNewlines)\n            UIPasteboard.general.string = content\n        }\n\n        showTips(text: \"Copy\".localized)\n        completion(.doNotDismiss)\n    }\n\n    /// 静音分组\n    func muteAction(_ response: UNNotificationResponse, completionHandler completion: @escaping (UNNotificationContentExtensionResponseOption) -> Void) {\n        let groupName = response.notification.request.content.threadIdentifier\n        // 静音一小时\n        GroupMuteSettingManager().settings[groupName] = Date() + 60 * 60\n\n        showTips(text: String(format: \"groupMuted\".localized, groupName.isEmpty ? \"default\".localized : groupName))\n        completion(.doNotDismiss)\n    }\n\n    func showTips(text: String) {\n        /// 调整页面整个大小为image的高度和label的高度总和\n        self.preferredContentSize = CGSize(width: 0, height: self.imageView.frame.height + 40)\n        self.noticeLabel.text = text\n        /// 调整 y的位置，如果复制内容，显示在图片的底部\n        self.noticeLabel.frame = CGRect(x: 0, y: self.imageView.frame.height, width: self.view.bounds.width, height: 40)\n    }\n}\n\nextension NotificationViewController {\n    ///  处理下拉显示大图\n    func ImageHandler(_ notification: UNNotification) {\n        Task {\n            guard let imageUrl = notification.request.content.userInfo[\"image\"] as? String,\n                  let imageFileUrl = await ImageDownloader.downloadImage(imageUrl),\n                  let image = UIImage(contentsOfFile: imageFileUrl)\n            else {\n                self.imageView.frame = .zero\n                return\n            }\n            /// 计算图片的比例按照通知界面缩放\n            let viewWidth = view.bounds.size.width\n            let aspectRatio = image.size.width / image.size.height\n            let viewHeight = viewWidth / aspectRatio\n            let size = CGSize(width: viewWidth, height: viewHeight)\n\n            DispatchQueue.main.async {\n                self.preferredContentSize = size\n                self.imageView.image = image\n                self.imageView.frame = CGRect(x: 0, y: 0, width: size.width, height: size.height)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "notificationContentExtension/UNNotificationContent+Extension.swift",
    "content": "//\n//  UNNotificationContent+Extension.swift\n//  NotificationContentExtension\n//\n//  Created by huangfeng on 8/26/25.\n//  Copyright © 2025 Fin. All rights reserved.\n//\n\nimport UIKit\n\nextension UNNotificationContent {\n    var bodyText: String {\n        guard let aps = self.userInfo[\"aps\"] as? [String: Any],\n              let alert = aps[\"alert\"] as? [String: Any],\n              let bodyText = alert[\"body\"] as? String\n        else {\n            return self.body\n        }\n        return bodyText\n    }\n}\n"
  }
]