{
  "format" : 1,
  "manifest" : {
    "capabilities" : [

    ],
    "homepage" : "https://github.com/navbytes/stackbar-plugins/tree/main/Productivity/WorldClock",
    "id" : "io.navbytes.stackbar.plugins.world-clock",
    "minimumStackbar" : "0.10.0",
    "name" : "World Clock",
    "summary" : "The time in the cities you work with, with offsets and working hours.",
    "surfaces" : [
      "menuBar",
      "panel",
      "settings",
      "widget"
    ],
    "triggers" : [
      {
        "launch" : {

        }
      },
      {
        "every" : {
          "seconds" : 30
        }
      },
      {
        "wake" : {

        }
      }
    ],
    "version" : "1.0.0"
  },
  "publisher" : {
    "name" : "navbytes",
    "publicKey" : "4dpLtXEpFXdi+6cviY/KdYuUulJPbEmCxxIK0EEN2/w="
  },
  "signature" : "WfD9b/pzQMWMEnQj4hAMpqA8eXythAse0VMtO5D1cB7nAVMLdGd5AjoHvLL9lQb6I1dx5iRnDqrx7aVTlRRHAw==",
  "source" : "import StackbarKit\nimport Foundation\n\n/// The time in the places you work with. The menu bar shows one city; the\n/// panel shows them all with the offset from you and whether it is working\n/// hours there. Cities are chosen in Settings. Needs nothing from the host.\n@main\nfinal class WorldClock: StackbarPlugin {\n    static let manifest = Manifest(\n        id: \"io.navbytes.stackbar.plugins.world-clock\",\n        name: \"World Clock\",\n        version: \"1.0.0\",\n        summary: \"The time in the cities you work with, with offsets and working hours.\",\n        surfaces: [.menuBar, .panel, .settings, .widget],\n        triggers: [.launch, .every(.seconds(30)), .wake],\n        minimumStackbar: \"0.10.0\",\n        homepage: \"https://github.com/navbytes/stackbar-plugins/tree/main/Productivity/WorldClock\"\n    )\n\n    var now = Date()\n    @Stored(\"zones\") var zones: [String] = [\"America/Los_Angeles\", \"America/New_York\", \"Europe/London\", \"Asia/Kolkata\"]\n    @Stored(\"menuBarZone\") var menuBarZone = \"Europe/London\"\n    @Stored(\"use24Hour\") var use24Hour = true\n    @Stored(\"workStart\") var workStart = 9.0\n    @Stored(\"workEnd\") var workEnd = 18.0\n\n    func refresh() async throws {\n        now = Date()\n    }\n\n    var menuBar: ViewContent {\n        if let zone = TimeZone(identifier: menuBarZone) {\n            Label(\"\\(city(menuBarZone)) \\(time(now, in: zone))\", symbol: \"globe\")\n        } else {\n            Label(\"World Clock\", symbol: \"globe\")\n        }\n    }\n\n    var panel: ViewContent {\n        if validZones.isEmpty {\n            ContentUnavailableView(\"No cities\", symbol: \"globe\", message: \"Add cities in Settings.\")\n        } else {\n            List {\n                ForEach(validZones) { zone in\n                    Row(city(zone.identifier), subtitle: subtitle(for: zone), symbol: isWorking(zone) ? \"briefcase\" : \"moon\", key: zone.identifier)\n                        .badge(time(now, in: zone), role: isWorking(zone) ? .success : .secondary)\n                        .action(\"Show in menu bar\") { self.menuBarZone = zone.identifier }\n                        .action(\"Copy time\") { Stackbar.copy(\"\\(self.city(zone.identifier)): \\(self.time(self.now, in: zone))\") }\n                }\n            }\n        }\n    }\n\n    var widget: ViewContent {\n        ForEach(validZones.prefix(4)) { zone in\n            LabeledContent(city(zone.identifier), value: time(now, in: zone))\n        }\n    }\n\n    var settings: ViewContent {\n        Section(\"Cities\") {\n            ForEach(zones) { identifier in\n                Row(city(identifier), subtitle: TimeZone(identifier: identifier) == nil ? \"Unknown time zone\" : identifier, symbol: \"mappin\", key: \"zone-\\(identifier)\")\n                    .action(\"Remove\") { self.zones.removeAll { $0 == identifier } }\n            }\n            TextField(\"Add a time zone, like Asia/Tokyo\", text: \"\") { submitted in self.add(submitted) }\n        }\n        Section(\"Menu bar\") {\n            Picker(\"City\", options: validZones.map(\\.identifier), selection: menuBarZone) { self.menuBarZone = $0 }\n            Toggle(\"24-hour clock\", isOn: use24Hour) { self.use24Hour = $0 }\n        }\n        Section(\"Working hours\") {\n            Slider(\"Start\", value: workStart, in: 0...23, step: 1) { self.workStart = $0 }\n            Slider(\"End\", value: workEnd, in: 1...24, step: 1) { self.workEnd = $0 }\n        }\n    }\n\n    var tools: [AgentTool] {\n        AgentTool(\"times\", description: \"The current time in each configured city\") { _ in\n            self.validZones.map { \"\\(self.city($0.identifier)): \\(self.time(self.now, in: $0))\" }.joined(separator: \"\\n\")\n        }\n    }\n\n    // MARK: -\n\n    private var validZones: [TimeZone] { zones.compactMap(TimeZone.init(identifier:)) }\n\n    private func add(_ text: String) {\n        let trimmed = text.trimmingCharacters(in: .whitespaces)\n        let match = TimeZone.knownTimeZoneIdentifiers.first { $0.caseInsensitiveCompare(trimmed) == .orderedSame }\n            ?? TimeZone.knownTimeZoneIdentifiers.first { city($0).caseInsensitiveCompare(trimmed) == .orderedSame }\n        guard let match, !zones.contains(match) else { return }\n        zones.append(match)\n    }\n\n    private func city(_ identifier: String) -> String {\n        (identifier.split(separator: \"/\").last.map(String.init) ?? identifier).replacingOccurrences(of: \"_\", with: \" \")\n    }\n\n    private func time(_ date: Date, in zone: TimeZone) -> String {\n        let formatter = DateFormatter()\n        formatter.locale = Locale(identifier: \"en_US_POSIX\")\n        formatter.timeZone = zone\n        formatter.dateFormat = use24Hour ? \"HH:mm\" : \"h:mm a\"\n        return formatter.string(from: date)\n    }\n\n    private func hour(in zone: TimeZone) -> Double {\n        var calendar = Calendar(identifier: .gregorian)\n        calendar.timeZone = zone\n        let parts = calendar.dateComponents([.hour, .minute], from: now)\n        return Double(parts.hour ?? 0) + Double(parts.minute ?? 0) / 60\n    }\n\n    private func isWorking(_ zone: TimeZone) -> Bool {\n        let weekday = { () -> Int in\n            var calendar = Calendar(identifier: .gregorian)\n            calendar.timeZone = zone\n            return calendar.component(.weekday, from: now)\n        }()\n        return (2...6).contains(weekday) && hour(in: zone) >= workStart && hour(in: zone) < workEnd\n    }\n\n    private func subtitle(for zone: TimeZone) -> String {\n        let difference = zone.secondsFromGMT(for: now) - TimeZone.current.secondsFromGMT(for: now)\n        let hours = Double(difference) / 3600\n        let offset = difference == 0 ? \"Same time\" : String(format: \"%@%g h\", hours > 0 ? \"+\" : \"−\", abs(hours))\n        var calendar = Calendar(identifier: .gregorian)\n        calendar.timeZone = zone\n        let theirDay = calendar.component(.day, from: now)\n        let ourDay = Calendar(identifier: .gregorian).component(.day, from: now)\n        let day = theirDay == ourDay ? \"\" : (zone.secondsFromGMT(for: now) > TimeZone.current.secondsFromGMT(for: now) ? \", tomorrow\" : \", yesterday\")\n        return offset + day\n    }\n}\n",
  "sourceDigest" : "97993fcd52c2719ef0a06283f3a9b583a0b1b7400c03c9bd9cc902bc9986b9b8"
}