Full Code of spencerwooo/PaimonMenuBar for AI

main 31ac8443a399 cached
66 files
123.6 KB
36.6k tokens
5 symbols
1 requests
Download .txt
Repository: spencerwooo/PaimonMenuBar
Branch: main
Commit: 31ac8443a399
Files: 66
Total size: 123.6 KB

Directory structure:
gitextract_kmzlkky9/

├── .gitignore
├── Docs/
│   ├── .eslintrc.json
│   ├── .gitignore
│   ├── README.md
│   ├── components/
│   │   ├── AvailableNow.tsx
│   │   ├── DownloadButton.tsx
│   │   ├── Faq.tsx
│   │   ├── Features.tsx
│   │   ├── Footer.tsx
│   │   ├── GitHubButton.tsx
│   │   ├── Head.tsx
│   │   ├── Hero.tsx
│   │   ├── HowToGetMyCookie.tsx
│   │   ├── PaimonCan.tsx
│   │   ├── PaimonCookie.tsx
│   │   ├── PaimonUses.tsx
│   │   ├── ReleaseInfo.tsx
│   │   └── Screenshot3D.tsx
│   ├── next-env.d.ts
│   ├── next.config.js
│   ├── package.json
│   ├── pages/
│   │   ├── _app.tsx
│   │   ├── api/
│   │   │   └── hello.ts
│   │   ├── index.tsx
│   │   └── types.d.ts
│   ├── postcss.config.js
│   ├── public/
│   │   └── site.webmanifest
│   ├── styles/
│   │   └── globals.css
│   ├── tailwind.config.js
│   └── tsconfig.json
├── LICENSE
├── PaimonMenuBar/
│   ├── AppDelegate.swift
│   ├── Assets.xcassets/
│   │   ├── AccentColor.colorset/
│   │   │   └── Contents.json
│   │   ├── AppIcon.appiconset/
│   │   │   └── Contents.json
│   │   ├── Commision.imageset/
│   │   │   └── Contents.json
│   │   ├── Contents.json
│   │   ├── Domain.imageset/
│   │   │   └── Contents.json
│   │   ├── Expedition.imageset/
│   │   │   └── Contents.json
│   │   ├── FragileResin.imageset/
│   │   │   └── Contents.json
│   │   ├── JarOfRiches.imageset/
│   │   │   └── Contents.json
│   │   ├── KokomiSad.imageset/
│   │   │   └── Contents.json
│   │   └── ParametricTransformer.imageset/
│   │       └── Contents.json
│   ├── Bundle.swift
│   ├── Compatibility.swift
│   ├── DataModels.swift
│   ├── Defaults+Workaround.swift
│   ├── Defaults.swift
│   ├── GameRecordUpdater.swift
│   ├── Info.plist
│   ├── MenuExtrasView.swift
│   ├── Networking.swift
│   ├── PaimonMenuBar.entitlements
│   ├── PaimonMenuBarApp.swift
│   ├── Preview Content/
│   │   └── Preview Assets.xcassets/
│   │       └── Contents.json
│   ├── SettingsView.swift
│   ├── UpdaterViewModel.swift
│   ├── en.lproj/
│   │   └── Localizable.strings
│   └── zh-Hans.lproj/
│       └── Localizable.strings
├── PaimonMenuBar.xcodeproj/
│   ├── project.pbxproj
│   ├── project.xcworkspace/
│   │   ├── contents.xcworkspacedata
│   │   └── xcshareddata/
│   │       ├── IDEWorkspaceChecks.plist
│   │       ├── WorkspaceSettings.xcsettings
│   │       └── swiftpm/
│   │           └── Package.resolved
│   └── xcshareddata/
│       └── xcschemes/
│           └── PaimonMenuBar.xcscheme
├── README.md
└── appcast.xml

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
# Xcode
#
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore

## User settings
xcuserdata/

## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
*.xcscmblueprint
*.xccheckout

## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
build/
DerivedData/
*.moved-aside
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3

## Obj-C/Swift specific
*.hmap

## App packaging
*.ipa
*.dSYM.zip
*.dSYM

## Playgrounds
timeline.xctimeline
playground.xcworkspace

# Swift Package Manager
#
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
# Packages/
# Package.pins
# Package.resolved
# *.xcodeproj
#
# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
# hence it is not needed unless you have added a package configuration file to your project
# .swiftpm

.build/

# CocoaPods
#
# We recommend against adding the Pods directory to your .gitignore. However
# you should judge for yourself, the pros and cons are mentioned at:
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
#
# Pods/
#
# Add this line if you want to avoid checking in source code from the Xcode workspace
# *.xcworkspace

# Carthage
#
# Add this line if you want to avoid checking in source code from Carthage dependencies.
# Carthage/Checkouts

Carthage/Build/

# Accio dependency management
Dependencies/
.accio/

# fastlane
#
# It is recommended to not store the screenshots in the git repo.
# Instead, use fastlane to re-generate the screenshots whenever they are needed.
# For more information about the recommended setup visit:
# https://docs.fastlane.tools/best-practices/source-control/#source-control

fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots/**/*.png
fastlane/test_output

# Code Injection
#
# After new code Injection tools there's a generated folder /iOSInjectionProject
# https://github.com/johnno1962/injectionforxcode

iOSInjectionProject/


================================================
FILE: Docs/.eslintrc.json
================================================
{
  "extends": "next/core-web-vitals"
}


================================================
FILE: Docs/.gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# local env files
.env*.local

# vercel
.vercel

# typescript
*.tsbuildinfo


================================================
FILE: Docs/README.md
================================================
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).

## Getting Started

First, run the development server:

```bash
npm run dev
# or
yarn dev
```

Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.

You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file.

[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`.

The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.

## Learn More

To learn more about Next.js, take a look at the following resources:

- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.

You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!

## Deploy on Vercel

The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.

Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.


================================================
FILE: Docs/components/AvailableNow.tsx
================================================
import type { AppReleaseData } from '../pages/types'
import Image from "next/image"

import DownloadButton from './DownloadButton'

import logo from '../images/logo.png'

const AvailableNow = ({ latest }: { latest: AppReleaseData }) => (
  <div className="relative py-8 px-4 md:px-8 border-y border-white/20 bg-[#232c33]">
    <div className="max-w-5xl mx-auto p-6 md:flex md:items-center md:justify-between">
      <div className="flex items-center">
        <Image className="-ml-4 mr-4" src={logo} alt="logo" height={120} width={120} />

        <div>
          <div className="font-bold font-mulish text-2xl md:text-4xl">
            Available on GitHub
          </div>
          <div className="font-medium opacity-60 mt-4 text-xs md:text-base">
            Requires macOS 11 Big Sur or later.
          </div>
        </div>
      </div>

      <DownloadButton
        tagName={latest.tag_name}
        downloadUrl={latest.assets[0].browser_download_url}
        tailwindStyles={'block mt-6 md:mt-0 py-2.5 text-center'}
      />
    </div>
  </div>
)
export default AvailableNow


================================================
FILE: Docs/components/DownloadButton.tsx
================================================
import { RiDownloadCloud2Line } from 'react-icons/ri'

const DownloadButton = ({
  tagName,
  downloadUrl,
  tailwindStyles,
}: {
  tagName: string
  downloadUrl: string
  tailwindStyles?: string
}) => (
  <a
    href={downloadUrl}
    target="_blank"
    rel="noopener noreferrer"
    className={`px-4 py-2 font-mulish rounded-lg lg:text-lg cursor-pointer bg-gradient-to-b from-slate-50 to-gray-300 text-slate-800 transition-all duration-150 hover:scale-105 hover:shadow-lg ${tailwindStyles}`}
  >
    Download <span className="font-bold">{tagName}</span>
    <RiDownloadCloud2Line className="inline ml-2" size={20} />
  </a>
)

export default DownloadButton


================================================
FILE: Docs/components/Faq.tsx
================================================
const Faq = () => (
  <article className="prose prose-invert max-w-none">
    <h2>FAQs</h2>

    <ol>
      <li>
        <b>Will I get banned from the game?</b>
        <br />
        No, I seriously do not think simply reading from an API (outside the
        game) constitutes as cheating.
      </li>

      <li>
        <b>I don&apos;t see a window? Where is the APP?</b>
        <br />
        It is a <b>menu bar</b> APP, check for resin icons that should appear in
        your menu bar.
      </li>

      <li>
        <b>
          Can I configure the refresh rate / data fetching frequency / polling
          interval?
        </b>
        <br />
        Yes - under <code>Preferences</code>.
      </li>

      <li>
        <b>Can I revert back to the original colored resin icon?</b>
        <br />
        Yes - use either one, configured under <code>Preferences</code>.
      </li>

      <li>
        <b>It is not working! Why?</b>
        <br />
        Most often it is your cookie problem. You need to make sure that you
        have turned on <code>Real-Time Notes</code> under{' '}
        <code>Battle Chronicle</code> inside{' '}
        <a
          href="https://www.hoyolab.com/article/1265396"
          target="_blank"
          rel="noopener noreferrer"
        >
          HoYoLAB
        </a>{' '}
        or <code>实时便签</code> inside 米游社, depending on your server. You
        would also have to set your profile to public (instructions for{' '}
        <a
          href="https://www.hoyolab.com/article/117720"
          target="_blank"
          rel="noopener noreferrer"
        >
          HoYoLAB
        </a>{' '}
        (a bit futher down the page) /{' '}
        <a
          href="https://www.9game.cn/yuanshen/5606032.html"
          target="_blank"
          rel="noopener noreferrer"
        >
          米游社
        </a>
        ).
      </li>
    </ol>
  </article>
)
export default Faq


================================================
FILE: Docs/components/Features.tsx
================================================
import Image, { StaticImageData } from "next/image"

import daily from '../images/daily.png'
import expedition from '../images/expedition.png'
import fragileResin from '../images/fragile-resin.png'
import weeklyBoss from '../images/weekly-boss.png'
import jarOfRiches from '../images/jar-of-riches.png'
import parametric from '../images/parametric-transformer.png'

const IconCard = ({
  icon,
  label,
  style,
}: {
  icon: StaticImageData
  label: string
  style: string
}) => (
  <div
    className={`rounded-lg lg:text-lg flex flex-col items-center justify-center py-6 ${style}`}
  >
    <Image
      src={icon}
      alt="icon"
      width={36}
      height={36}
      style={{
        maxWidth: "100%",
        height: "auto"
      }} />
    <span className="mt-2">{label}</span>
  </div>
)

const Features = () => (
  <div className="relative grid grid-cols-2 md:grid-cols-3 2xl:grid-cols-6 gap-4 md:gap-8 mt-12 py-16 px-4 md:px-8 border-y border-white/20 bg-[#232c33]">
    <IconCard
      icon={fragileResin}
      label="Resin"
      style="bg-blue-900/30 text-blue-200"
    />
    <IconCard
      icon={daily}
      label="Daily Commissions"
      style="bg-purple-900/30 text-purple-200"
    />
    <IconCard
      icon={expedition}
      label="Expeditions"
      style="bg-orange-900/30 text-orange-200"
    />
    <IconCard
      icon={weeklyBoss}
      label="Weekly Bosses"
      style="bg-yellow-900/30 text-amber-200"
    />
    <IconCard
      icon={jarOfRiches}
      label="Realm Currency"
      style="bg-gray-900/30 text-gray-200"
    />
    <IconCard
      icon={parametric}
      label="Parametric Transformer"
      style="bg-teal-900/30 text-teal-200"
    />
  </div>
)

export default Features


================================================
FILE: Docs/components/Footer.tsx
================================================
import { RiHeartPulseLine, RiHeartsLine } from 'react-icons/ri'

const Footer = () => (
  <div className="relative px-4 py-8 text-sm text-center border-t border-white/20 bg-[#232c33] w-full">
    <div>
      Created with love by{' '}
      <a
        href="https://spencerwoo.com"
        target="_blank"
        rel="noopener noreferrer"
        className="text-[#9CA6A0] underline hover:opacity-90"
      >
        Spencer Woo
      </a>{' '}
    </div>
    <div>
      <RiHeartPulseLine className="inline" /> Love and Kisses from Hu Tao{' '}
      <RiHeartsLine className="inline" />
    </div>
  </div>
)

export default Footer


================================================
FILE: Docs/components/GitHubButton.tsx
================================================
import { RiGithubLine } from 'react-icons/ri'

const GitHubButton = () => {
  return (
    <a
      className="px-4 py-2 rounded-lg font-mulish lg:text-lg cursor-pointer border border-gradient-to-b from-slate-50 to-gray-300 transition-all duration-150 hover:scale-105 hover:shadow-lg"
      href="https://github.com/spencerwooo/PaimonMenuBar"
      target="_blank"
      rel="noopener noreferrer"
    >
      GitHub
      <RiGithubLine className="inline ml-2" size={20} />
    </a>
  )
}

export default GitHubButton


================================================
FILE: Docs/components/Head.tsx
================================================
import Head from 'next/head'

const Meta = () => (
  <Head>
    <title>PaimonMenuBar</title>
    <meta name="description" content="Paimon is now in your macOS menubar!" />
    <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
    <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
    <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
    <link rel="manifest" href="/site.webmanifest" />
  </Head>
)

export default Meta


================================================
FILE: Docs/components/Hero.tsx
================================================
import type { AppReleaseData } from '../pages/types'

import Image from "next/image"

import Screenshot3D from '../components/Screenshot3D'
import DownloadButton from '../components/DownloadButton'
import ReleaseInfo from '../components/ReleaseInfo'
import GitHubButton from '../components/GitHubButton'

import logo from '../images/logo.png'

const Hero = ({ latest }: { latest: AppReleaseData }) => (
  <div className="relative max-w-5xl p-6 mx-auto pt-28">
    <div className="hidden md:block float-right">
      <Screenshot3D />
    </div>

    <div className="flex items-center">
      <Image className="-ml-4" src={logo} alt="logo" height={72} width={72} priority />

      <span className="text-3xl lg:text-5xl font-bold tracking-wide ml-4 font-mulish inline">
        PaimonMenuBar
      </span>
    </div>

    <div className="mt-16 text-xl lg:text-3xl max-w-lg tracking-wide">
      Track your Genshin Impact daily resin, expeditions, and more — straight in
      your macOS menu bar.
    </div>

    <div className="text-lg mt-16">
      <div className="opacity-60 text-xs lg:text-base mb-4">
        Made with SwiftUI, designed for macOS. Works with —
      </div>

      <div className="flex items-center space-x-4">
        <div className="border rounded-lg p-2">
          <div className="text-xs">天空岛 | 世界树</div>
          <div className="font-bold font-mulish tracking-wider">🇨🇳 CN</div>
        </div>
        <div className="opacity-60">&</div>
        <div className="border rounded-lg p-2">
          <div className="text-xs">NA | EU | Asia | SAR</div>
          <div className="font-bold font-mulish tracking-wider">🌍 Global</div>
        </div>
      </div>
    </div>

    <div className="inline-flex items-center space-x-4 mt-16">
      <DownloadButton
        tagName={latest.tag_name}
        downloadUrl={latest.assets[0].browser_download_url}
      />
      <GitHubButton />
    </div>

    <div className="font-medium opacity-80 mt-4">
      Requires macOS 11 Big Sur or later.
    </div>

    <ReleaseInfo
      htmlUrl={latest.html_url}
      publishedAt={latest.published_at}
      downloadCount={latest.assets[0].download_count}
      reactions={latest.reactions}
    />
  </div>
)
export default Hero


================================================
FILE: Docs/components/HowToGetMyCookie.tsx
================================================
import Image from "next/image"

import hutaoSleepy from '../images/hutao-sleepy.png'
import cookieScreenshot from '../images/cookie.jpg'
import configScreenshot from '../images/config-screenshot.jpg'

const HowToGetMyCookie = () => (
  <div>
    <Image
      className="float-right sticky top-4 hidden md:block"
      src={hutaoSleepy}
      alt="emoji hutao"
      width={128}
      height={128} />

    <section
      id="how-to-get-my-cookie"
      className="prose prose-invert prose-img:rounded prose-figcaption:text-center"
    >
      <h2>How to get my cookie?</h2>

      <p>
        Open{' '}
        <a
          href="https://bbs.mihoyo.com/ys"
          target="_blank"
          rel="noopener noreferrer"
        >
          https://bbs.mihoyo.com/ys
        </a>{' '}
        (if you are on 天空岛 or 世界树) or{' '}
        <a
          href="https://www.hoyolab.com/home"
          target="_blank"
          rel="noopener noreferrer"
        >
          https://hoyolab.com/home
        </a>{' '}
        (if you are on NA | EU | Asia | SAR) in <b>Chrome</b>, login, and press{' '}
        <kbd>F12</kbd> to open Chrome devtools.
      </p>

      <p>
        Navigate to <code>Console</code>, type in <code>document.cookie</code>{' '}
        and press <kbd>Enter</kbd>:
      </p>

      <figure>
        <Image
          src={cookieScreenshot}
          alt="Cookie screenshot"
          style={{
            maxWidth: "100%",
            height: "auto"
          }} />
        <figcaption>Getting your cookie from 米游社 or HoYoLAB</figcaption>
      </figure>

      <p>
        Copy the string <i>without the quotes</i> and paste it inside{' '}
        <code>PaimonMenuBar</code> under{' '}
        <code>Preferences {'>'} Configuration</code>, and test your config:
      </p>

      <figure>
        <Image
          src={configScreenshot}
          alt="Config screenshot"
          style={{
            maxWidth: "100%",
            height: "auto"
          }} />
        <figcaption>Putting your cookie in PaimonMenuBar</figcaption>
      </figure>

      <p>
        You should be able to see the updated data inside the app - which will
        periodically update if your config stays valid, enjoy!
      </p>
    </section>
  </div>
)
export default HowToGetMyCookie


================================================
FILE: Docs/components/PaimonCan.tsx
================================================
import Image from "next/image"
import paimonMighty from '../images/paimon-mighty.png'

const PaimonCan = () => (
  <div>
    <Image
      className="float-right sticky top-4 hidden md:block"
      src={paimonMighty}
      alt="paimon emoji"
      width={128}
      height={128} />

    <section className="prose prose-invert">
      <h2>Mighty Paimon!</h2>

      <p>Paimon can help you —</p>

      <ul>
        <li>Keep track of your daily resin.</li>
        <li>Monitor your daily expeditions and real-time realm currency.</li>
        <li>Remind you about your daily commissions and weekly boss fights.</li>
        <li>
          And notify you when your parametric transformer is ready to use.
        </li>
      </ul>

      <p>
        Basically, Paimon lives in your macOS menu bar quietly, and offers you a
        nice way of monitoring your in-game real-time stats when you need to
        check them.
      </p>
    </section>
  </div>
)

export default PaimonCan


================================================
FILE: Docs/components/PaimonCookie.tsx
================================================
import Image from "next/image"
import luminePlease from '../images/lumine-please.png'

const PaimonCookie = () => (
  <div>
    <Image
      className="float-right sticky top-4 hidden md:block"
      src={luminePlease}
      alt="lumine emoji"
      width={128}
      height={128} />

    <section className="prose prose-invert">
      <h2>Why does Paimon need your cookie?</h2>
      <p>
        Cookies are sensitive information, and in some scenarios they function
        as your login credentials. Paimon requires your cookie so that Paimon
        can request said API on your behalf, and fetch those in-game stats
        periodically.
      </p>
      <p>
        Paimon will <b>never-ever-ever-ever</b> ask for your credentials to
        Genshin Impact nor any account! The cookie is{' '}
        <b>only stored locally.</b>
      </p>
    </section>
  </div>
)
export default PaimonCookie


================================================
FILE: Docs/components/PaimonUses.tsx
================================================
import Image from "next/image"
import zhongliThink from '../images/zhongli-think.png'

const PaimonUses = () => (
  <div>
    <Image
      className="float-right sticky top-4 hidden md:block"
      src={zhongliThink}
      alt="zhongli emoji"
      width={128}
      height={128} />

    <section className="prose prose-invert">
      <h2>How does Paimon work?</h2>
      <p>
        Paimon uses the official Mihoyo / Hoyoverse API found in either{' '}
        <a
          href="https://bbs.mihoyo.com/ys/"
          target="_blank"
          rel="noopener noreferrer"
        >
          米游社 (for CN players)
        </a>{' '}
        or{' '}
        <a
          href="https://www.hoyolab.com/home"
          target="_blank"
          rel="noopener noreferrer"
        >
          HoYoLAB (for Global players)
        </a>
        .
      </p>
    </section>
  </div>
)
export default PaimonUses


================================================
FILE: Docs/components/ReleaseInfo.tsx
================================================
import type { AppReleaseData } from '../pages/types'

const reactionToEmoji = {
  '+1': '👍',
  '-1': '👎',
  laugh: '😂',
  confused: '😕',
  heart: '❤️',
  hooray: '🎉',
  rocket: '🚀',
  eyes: '👀',
} as const
type reactionKeys = keyof typeof reactionToEmoji

const formatRelativeDate = (publishedAt: string) => {
  const publishedDate = new Date(publishedAt)
  const deltaTime = (publishedDate.getTime() - Date.now()) / 1000

  const formatter = new Intl.RelativeTimeFormat()
  if (deltaTime > -60 * 60) {
    return formatter.format(Math.floor(deltaTime / 60), 'minute')
  }
  if (deltaTime > -24 * 60 * 60) {
    return formatter.format(Math.floor(deltaTime / 60 / 60), 'hour')
  }
  if (deltaTime > -7 * 24 * 60 * 60) {
    return formatter.format(Math.floor(deltaTime / 60 / 60 / 24), 'day')
  }
  return formatter.format(Math.floor(deltaTime / 60 / 60 / 24 / 7), 'week')
}

const ReleaseInfo = ({
  htmlUrl,
  publishedAt,
  downloadCount,
  reactions,
}: {
  htmlUrl: string
  publishedAt: string
  downloadCount: number
  reactions: AppReleaseData['reactions']
}) => {
  const reactionsNonZero = Object.entries(reactions ?? {}).filter(
    ([key, count]) => key !== 'total_count' && count > 0
  ) as Array<[reactionKeys, number]>

  return (
    <>
      <a
        href={htmlUrl}
        target="_blank"
        rel="noopener noreferrer"
        className="text-sm mt-4 opacity-50 hover:opacity-60"
      >
        Last updated {formatRelativeDate(publishedAt)}. Downloads:{' '}
        {downloadCount}.
        <div className="flex md:inline-flex mt-2 md:ml-2 items-center gap-2 text-xs">
          {reactionsNonZero.map(
            ([key, val]: [key: reactionKeys, val: number]) => (
              <span
                key={key}
                className="rounded-lg border border-slate-50/40 px-2 py-0.5"
              >
                {reactionToEmoji[key]} {val}
              </span>
            )
          )}
        </div>
      </a>
    </>
  )
}

export default ReleaseInfo


================================================
FILE: Docs/components/Screenshot3D.tsx
================================================
import Image from "next/image"
import Atropos from 'atropos/react'
import screenshot from '../images/screenshot-transparent-light.png'

const Screenshot3D = () => (
  <Atropos className="w-[360px] h-[592.8]" shadow={false} highlight={false}>
    <Image
      src={screenshot}
      alt="PaimonMenuBar screenshot"
      width={360}
      height={592.8}
      style={{
        maxWidth: "100%",
        height: "auto"
      }} />
  </Atropos>
)

export default Screenshot3D


================================================
FILE: Docs/next-env.d.ts
================================================
/// <reference types="next" />
/// <reference types="next/image-types/global" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.


================================================
FILE: Docs/next.config.js
================================================
/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,

}

module.exports = nextConfig


================================================
FILE: Docs/package.json
================================================
{
  "name": "docs",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "@fontsource/mulish": "^4.5.14",
    "atropos": "^1.0.2",
    "next": "13.2.4",
    "react": "18.2.0",
    "react-dom": "18.2.0",
    "react-icons": "^4.8.0"
  },
  "devDependencies": {
    "@tailwindcss/typography": "^0.5.9",
    "@types/node": "18.15.0",
    "@types/react": "18.0.28",
    "@types/react-dom": "18.0.11",
    "autoprefixer": "^10.4.14",
    "eslint": "8.36.0",
    "eslint-config-next": "13.2.4",
    "postcss": "^8.4.21",
    "tailwindcss": "^3.2.7",
    "typescript": "4.9.5"
  }
}


================================================
FILE: Docs/pages/_app.tsx
================================================
import '../styles/globals.css'
import 'atropos/css'

import '@fontsource/mulish/400.css'
import '@fontsource/mulish/700.css'

import type { AppProps } from 'next/app'

function MyApp({ Component, pageProps }: AppProps) {
  return <Component {...pageProps} />
}

export default MyApp


================================================
FILE: Docs/pages/api/hello.ts
================================================
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next'

type Data = {
  name: string
}

export default function handler(
  req: NextApiRequest,
  res: NextApiResponse<Data>
) {
  res.status(200).json({ name: 'Paimon says hi!' })
}


================================================
FILE: Docs/pages/index.tsx
================================================
import type { GetStaticProps } from 'next'
import type { AppReleaseData } from './types'
import Image from "next/image"

import Features from '../components/Features'
import PaimonCan from '../components/PaimonCan'
import PaimonUses from '../components/PaimonUses'
import PaimonCookie from '../components/PaimonCookie'
import HowToGetMyCookie from '../components/HowToGetMyCookie'
import Footer from '../components/Footer'
import Hero from '../components/Hero'
import Meta from '../components/Head'

import hutaoBackground from '../images/hutao-bg.jpg'
import AvailableNow from '../components/AvailableNow'
import Faq from '../components/Faq'

const Home = ({ latest }: { latest: AppReleaseData }) => {
  return <>
    <Meta />

    <main className="text-white relative">
      <div className="absolute w-full">
        <Image
          src={hutaoBackground}
          alt="background"
          placeholder="blur"
          priority
          sizes="100vw"
          style={{
            width: "100%",
            height: "auto"
          }} />
        <div className="absolute top-0 bottom-0 left-0 right-0 bg-gradient-to-b from-transparent to-[#2C3740]" />
      </div>

      <Hero latest={latest} />

      <Features />

      <div className="bg-[#2c3740] relative">
        <div className="max-w-5xl px-6 py-20 mx-auto">
          <div className="space-y-16">
            <PaimonCan />
            <PaimonUses />
            <PaimonCookie />
            <HowToGetMyCookie />
          </div>
        </div>

        <AvailableNow latest={latest} />

        <div className="max-w-5xl px-6 py-20 mx-auto">
          <Faq />
        </div>

        <Footer />
      </div>
    </main>
  </>;
}

export const getStaticProps: GetStaticProps = async () => {
  const resp = await fetch(
    'https://api.github.com/repos/spencerwooo/PaimonMenuBar/releases/latest'
  )
  const latest = (await resp.json()) as AppReleaseData
  return { props: { latest }, revalidate: 5 * 60 }
}

export default Home


================================================
FILE: Docs/pages/types.d.ts
================================================
export type AppReleaseData = {
  html_url: string
  tag_name: string
  name: string
  published_at: string
  assets: Array<{
    size: number
    download_count: number
    browser_download_url: string
  }>
  reactions?: {
    total_count: number
    '+1': number
    '-1': number
    laugh: number
    confused: number
    heart: number
    hooray: number
    rocket: number
    eyes: number
  }
}


================================================
FILE: Docs/postcss.config.js
================================================
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
}


================================================
FILE: Docs/public/site.webmanifest
================================================
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}

================================================
FILE: Docs/styles/globals.css
================================================
@tailwind base;
@tailwind components;
@tailwind utilities;

body {
  background-color: #2c3740;
}


================================================
FILE: Docs/tailwind.config.js
================================================
const defaultTheme = require('tailwindcss/defaultTheme')

module.exports = {
  content: [
    "./pages/**/*.{js,ts,jsx,tsx}",
    "./components/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {
      fontFamily: {
        "mulish": ["Mulish", ...defaultTheme.fontFamily.sans],
      }
    },
  },
  plugins: [require('@tailwindcss/typography')],
}


================================================
FILE: Docs/tsconfig.json
================================================
{
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
  "exclude": ["node_modules"]
}


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2022 Spencer Woo

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: PaimonMenuBar/AppDelegate.swift
================================================
//
//  AppDelegate.swift
//  PaimonMenuBar
//
//  Created by Spencer Woo on 2022/3/23.
//

import AppKit
import Defaults
import Foundation
import SwiftUI
import UserNotifications

final class AppDelegate: NSObject, NSApplicationDelegate {
    private(set) static var shared: AppDelegate!

    private var statusItem: NSStatusItem!
    private var statusButton: NSStatusBarButton!
    private var menuItemMain: NSHostingView<MenuExtrasView>!

    /** updateStatusBar and updateStatusIcon must be called in the main thread to avoid race condition. */
    func updateStatusBar() {
        assert(Thread.isMainThread)

        updateStatusIcon()
        updateStatusButtonTitle()

        let gameRecord = Defaults[.lastGameRecord]
        let currentExpeditionNum = gameRecord.data.current_expedition_num
        menuItemMain.frame = NSRect(x: 0, y: 0, width: 290, height: 292 + currentExpeditionNum * 36)
    }

    func updateStatusIcon() {
        assert(Thread.isMainThread)

        statusButton.imagePosition = NSControl.ImagePosition.imageLeading
        statusButton.image = NSImage(named: NSImage.Name("FragileResin"))

        // This sets the resin icon in the statusbar as monochrome if isTemplate == true
        statusButton.image?.isTemplate = Defaults[.isStatusIconTemplate]

        statusButton.image?.size.width = 19
        statusButton.image?.size.height = 19
    }

    func updateStatusButtonTitle() {
        if !Defaults[.isShowResinText] {
            statusButton.title = ""
            return
        }

        let gameRecord = Defaults[.lastGameRecord]
        if gameRecord.fetchAt == nil {
            statusButton.title = "-/160" // Cookie Not configured
        } else {
            statusButton.title = "\(gameRecord.data.current_resin)/\(gameRecord.data.max_resin)"
        }
    }

    @objc private func openSettingsView() {
        if #available(macOS 13, *) {
            NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil)
        } else {
            NSApp.sendAction(Selector(("showPreferencesWindow:")), to: nil, from: nil)
        }

        NSApp.setActivationPolicy(.regular)
        NSApp.activate(ignoringOtherApps: true)
        NSApp.windows.first?.makeKeyAndOrderFront(self)
    }

    func applicationDidFinishLaunching(_: Notification) {
        AppDelegate.shared = self

        // Request notification permissions
        UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { _, error in
            if error != nil {
                print("Notification permission not granted.")
            } else {
                print("Notification permission granted.")
            }
        }

        // Update game record on initial launch
        print("App is started")
        GameRecordUpdater.shared.tryFetchGameRecordAndRender()

        // Close main APP window on initial launch
        NSApp.setActivationPolicy(.accessory)
        if let window = NSApplication.shared.windows.first {
            window.close()
        }

        setupStatusBar()
    }

    func applicationShouldTerminateAfterLastWindowClosed(_: NSApplication) -> Bool {
        // Hide app icon in dock after all windows are closed
        NSApp.setActivationPolicy(.accessory)
        return false
    }

    private func setupStatusBar() {
        let menu = NSMenu()

        // Main menu area, render view as NSHostingView
        menuItemMain = NSHostingView(rootView: MenuExtrasView())
        let menuItem = NSMenuItem()
        menuItem.view = menuItemMain
        menu.addItem(menuItem)

        // Submenu, preferences, and quit APP
        menu.addItem(NSMenuItem.separator())
        menu
            .addItem(NSMenuItem(title: String.localized("Preferences"), action: #selector(openSettingsView),
                                keyEquivalent: ","))
        menu
            .addItem(NSMenuItem(title: String.localized("Quit"), action: #selector(NSApplication.terminate(_:)),
                                keyEquivalent: "q"))

        statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
        statusItem.menu = menu
        statusButton = statusItem.button

        updateStatusBar()
    }
}


================================================
FILE: PaimonMenuBar/Assets.xcassets/AccentColor.colorset/Contents.json
================================================
{
  "colors" : [
    {
      "idiom" : "universal"
    }
  ],
  "info" : {
    "author" : "xcode",
    "version" : 1
  }
}


================================================
FILE: PaimonMenuBar/Assets.xcassets/AppIcon.appiconset/Contents.json
================================================
{
  "images" : [
    {
      "filename" : "hutao16@1.png",
      "idiom" : "mac",
      "scale" : "1x",
      "size" : "16x16"
    },
    {
      "filename" : "hutao16@2.png",
      "idiom" : "mac",
      "scale" : "2x",
      "size" : "16x16"
    },
    {
      "filename" : "hutao16@2-1.png",
      "idiom" : "mac",
      "scale" : "1x",
      "size" : "32x32"
    },
    {
      "filename" : "hutao@64.png",
      "idiom" : "mac",
      "scale" : "2x",
      "size" : "32x32"
    },
    {
      "filename" : "hutao@128.png",
      "idiom" : "mac",
      "scale" : "1x",
      "size" : "128x128"
    },
    {
      "filename" : "hutao@256.png",
      "idiom" : "mac",
      "scale" : "2x",
      "size" : "128x128"
    },
    {
      "filename" : "hutao@256-1.png",
      "idiom" : "mac",
      "scale" : "1x",
      "size" : "256x256"
    },
    {
      "filename" : "hutao@512.png",
      "idiom" : "mac",
      "scale" : "2x",
      "size" : "256x256"
    },
    {
      "filename" : "hutao@512-1.png",
      "idiom" : "mac",
      "scale" : "1x",
      "size" : "512x512"
    },
    {
      "filename" : "hutao.png",
      "idiom" : "mac",
      "scale" : "2x",
      "size" : "512x512"
    }
  ],
  "info" : {
    "author" : "xcode",
    "version" : 1
  }
}


================================================
FILE: PaimonMenuBar/Assets.xcassets/Commision.imageset/Contents.json
================================================
{
  "images" : [
    {
      "filename" : "commision.svg",
      "idiom" : "universal",
      "scale" : "1x"
    },
    {
      "idiom" : "universal",
      "scale" : "2x"
    },
    {
      "idiom" : "universal",
      "scale" : "3x"
    }
  ],
  "info" : {
    "author" : "xcode",
    "version" : 1
  }
}


================================================
FILE: PaimonMenuBar/Assets.xcassets/Contents.json
================================================
{
  "info" : {
    "author" : "xcode",
    "version" : 1
  }
}


================================================
FILE: PaimonMenuBar/Assets.xcassets/Domain.imageset/Contents.json
================================================
{
  "images" : [
    {
      "filename" : "Domain.png",
      "idiom" : "universal",
      "scale" : "1x"
    },
    {
      "filename" : "Domain@2x.png",
      "idiom" : "universal",
      "scale" : "2x"
    },
    {
      "filename" : "Domain@3x.png",
      "idiom" : "universal",
      "scale" : "3x"
    }
  ],
  "info" : {
    "author" : "xcode",
    "version" : 1
  }
}


================================================
FILE: PaimonMenuBar/Assets.xcassets/Expedition.imageset/Contents.json
================================================
{
  "images" : [
    {
      "filename" : "Expedition.png",
      "idiom" : "universal",
      "scale" : "1x"
    },
    {
      "filename" : "Expedition@2x.png",
      "idiom" : "universal",
      "scale" : "2x"
    },
    {
      "filename" : "Expedition@3x.png",
      "idiom" : "universal",
      "scale" : "3x"
    }
  ],
  "info" : {
    "author" : "xcode",
    "version" : 1
  }
}


================================================
FILE: PaimonMenuBar/Assets.xcassets/FragileResin.imageset/Contents.json
================================================
{
  "images" : [
    {
      "filename" : "fragile_resin@1.png",
      "idiom" : "universal",
      "scale" : "1x"
    },
    {
      "filename" : "fragile_resin@2.png",
      "idiom" : "universal",
      "scale" : "2x"
    },
    {
      "filename" : "fragile_resin@3.png",
      "idiom" : "universal",
      "scale" : "3x"
    }
  ],
  "info" : {
    "author" : "xcode",
    "version" : 1
  }
}


================================================
FILE: PaimonMenuBar/Assets.xcassets/JarOfRiches.imageset/Contents.json
================================================
{
  "images" : [
    {
      "filename" : "JarOfRiches.png",
      "idiom" : "universal",
      "scale" : "1x"
    },
    {
      "filename" : "JarOfRiches@2x.png",
      "idiom" : "universal",
      "scale" : "2x"
    },
    {
      "filename" : "JarOfRiches@3x.png",
      "idiom" : "universal",
      "scale" : "3x"
    }
  ],
  "info" : {
    "author" : "xcode",
    "version" : 1
  }
}


================================================
FILE: PaimonMenuBar/Assets.xcassets/KokomiSad.imageset/Contents.json
================================================
{
  "images" : [
    {
      "filename" : "kokomi_sad.png",
      "idiom" : "universal",
      "scale" : "1x"
    },
    {
      "filename" : "kokomi_sad@2.png",
      "idiom" : "universal",
      "scale" : "2x"
    },
    {
      "filename" : "kokomi_sad@3.png",
      "idiom" : "universal",
      "scale" : "3x"
    }
  ],
  "info" : {
    "author" : "xcode",
    "version" : 1
  }
}


================================================
FILE: PaimonMenuBar/Assets.xcassets/ParametricTransformer.imageset/Contents.json
================================================
{
  "images" : [
    {
      "filename" : "ParametricTransformer@1.png",
      "idiom" : "universal",
      "scale" : "1x"
    },
    {
      "filename" : "ParametricTransformer@2.png",
      "idiom" : "universal",
      "scale" : "2x"
    },
    {
      "filename" : "ParametricTransformer@3.png",
      "idiom" : "universal",
      "scale" : "3x"
    }
  ],
  "info" : {
    "author" : "xcode",
    "version" : 1
  }
}


================================================
FILE: PaimonMenuBar/Bundle.swift
================================================
//
//  Bundle.swift
//  PaimonMenuBar
//
//  Created by Spencer Woo on 2022/3/26.
//

import Foundation

extension Bundle {
    var appName: String? {
        return object(forInfoDictionaryKey: "CFBundleName") as? String
    }

    var appVersion: String? {
        return object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String
    }

    var buildNumber: String? {
        return object(forInfoDictionaryKey: "CFBundleVersion") as? String
    }
}


================================================
FILE: PaimonMenuBar/Compatibility.swift
================================================
//
//  Compatibility.swift
//  PaimonMenuBar
//
//  Created by DreamPiggy on 2022/5/6.
//

import Foundation
import SwiftUI

extension URLSession {
    @available(macOS, deprecated: 12.0, message: "This extension is no longer necessary. Use API built into SDK")
    func data(for request: URLRequest) async throws -> (Data, URLResponse) {
        try await withCheckedThrowingContinuation { continuation in
            let task = self.dataTask(with: request) { data, response, error in
                guard let data = data, let response = response else {
                    let error = error ?? URLError(.badServerResponse)
                    return continuation.resume(throwing: error)
                }

                continuation.resume(returning: (data, response))
            }

            task.resume()
        }
    }
}

extension String {
    @available(macOS, deprecated: 12.0, message: "This extension is no longer necessary. Use API built into SDK")
    static func localized(
        _ keyAndValue: LocalizedStringKey,
        table: String? = nil,
        bundle: Bundle? = nil,
        locale: Locale = .current,
        comment: StaticString? = nil
    ) -> String {
        var language = "en"
        // Region: CN
        // ID: zh-Hans-CN
        // Need: zh-Hans
        if let localRegion = locale.regionCode, let localID = locale.collatorIdentifier,
           let range = localID.range(of: localRegion)
        {
            language = String(localID[..<range.lowerBound])
            language.removeLast()
        }

        var localBundle = Bundle.main
        if let path = (bundle ?? Bundle.main).path(forResource: language, ofType: "lproj") {
            localBundle = Bundle(path: path) ?? .main
        }

        let mirror = Mirror(reflecting: keyAndValue)
        let attributeLabelAndValue = mirror.children.first { arg0 -> Bool in
            let (label, _) = arg0
            if label == "key" {
                return true
            }
            return false
        }
        guard let key = attributeLabelAndValue?.value as? String else {
            fatalError()
        }
        return NSLocalizedString(key, tableName: table, bundle: localBundle, value: "", comment: "\(comment ?? "")")
    }
}

extension Date {
    private static let shortenedFormatter: DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateStyle = .none
        formatter.timeStyle = .short
        return formatter
    }()

    private static let defaultFormatter: DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateStyle = .short
        formatter.timeStyle = .short
        return formatter
    }()

    var shortenedFormatted: String {
        return Date.shortenedFormatter.string(from: self)
    }

    var defaultFormatted: String {
        return Date.defaultFormatter.string(from: self)
    }
}


================================================
FILE: PaimonMenuBar/DataModels.swift
================================================
//
//  DataModels.swift
//  PaimonMenuBar
//
//  Created by Spencer Woo on 2022/3/24.
//

import Defaults
import Foundation

struct GameRecord: Codable, Defaults.Serializable {
    /**
     This field is not returned by server. Instead, it is set by us after fetching.
     When this field is not present, it means the record is not a real record (e.g. empty record).
     */
    var fetchAt: Date?

    var retcode: Int
    var message: String

    var data: GameData

    static let empty = GameRecord(
        fetchAt: nil, // Indicate an empty record
        retcode: 0,
        message: "OK",
        data: GameData(
            current_resin: 0, max_resin: 160, resin_recovery_time: "0", finished_task_num: 0,
            total_task_num: 4, is_extra_task_reward_received: false, remain_resin_discount_num: 0,
            resin_discount_num_limit: 3, current_expedition_num: 1, max_expedition_num: 5,
            expeditions: [Expeditions(status: "Finished", avatar_side_icon: "", remained_time: "0")],
            current_home_coin: 0, max_home_coin: 2400, home_coin_recovery_time: "0", calendar_url: "",
            transformer: Transformer(
                obtained: true,
                recovery_time: RecoveryTime(Day: 0, Hour: 0, Minute: 0, Second: 0, reached: false), wiki: ""
            )
        )
    )
}

struct GameData: Codable, Defaults.Serializable {
    var current_resin: Int
    var max_resin: Int
    var resin_recovery_time: String
    var finished_task_num: Int
    var total_task_num: Int
    var is_extra_task_reward_received: Bool
    var remain_resin_discount_num: Int
    var resin_discount_num_limit: Int
    var current_expedition_num: Int
    var max_expedition_num: Int

    var expeditions: [Expeditions]

    var current_home_coin: Int
    var max_home_coin: Int
    var home_coin_recovery_time: String
    var calendar_url: String

    var transformer: Transformer
}

struct Expeditions: Codable, Hashable, Defaults.Serializable {
    var status: String
    var avatar_side_icon: String
    var remained_time: String
}

struct Transformer: Codable, Defaults.Serializable {
    var obtained: Bool
    var recovery_time: RecoveryTime
    var wiki: String
}

struct RecoveryTime: Codable, Defaults.Serializable {
    var Day: Int
    var Hour: Int
    var Minute: Int
    var Second: Int
    var reached: Bool
}

enum GenshinServer: String, CaseIterable, Identifiable, Defaults.Serializable {
    case cn_gf01 // 天空岛
    case cn_qd01 // 世界树

    case os_usa // Global (NA)
    case os_euro // Global (EU)
    case os_asia // Global (Asia)
    case os_cht // Global (SAR)

    var id: String { rawValue }

    var serverName: String {
        switch self {
        case .cn_gf01:
            return "天空岛"
        case .cn_qd01:
            return "世界树"
        case .os_usa:
            return "NA"
        case .os_euro:
            return "EU"
        case .os_asia:
            return "Asia"
        case .os_cht:
            return "SAR"
        }
    }

    var isCNServer: Bool {
        switch self {
        case .cn_gf01, .cn_qd01:
            return true
        default:
            return false
        }
    }

    var cookieSiteUrl: String {
        switch self {
        case .cn_gf01, .cn_qd01:
            return "https://bbs.mihoyo.com/ys"
        case .os_usa, .os_euro, .os_asia, .os_cht:
            return "https://www.hoyolab.com/home"
        }
    }
}


================================================
FILE: PaimonMenuBar/Defaults+Workaround.swift
================================================
// See https://github.com/sindresorhus/Defaults/blob/main/workaround.md

import Defaults
import Foundation

public extension Defaults.Serializable where Self: Codable {
    static var bridge: Defaults.TopLevelCodableBridge<Self> { Defaults.TopLevelCodableBridge() }
}

public extension Defaults.Serializable where Self: Codable & NSSecureCoding {
    static var bridge: Defaults.CodableNSSecureCodingBridge<Self> { Defaults.CodableNSSecureCodingBridge() }
}

public extension Defaults.Serializable where Self: Codable & NSSecureCoding & Defaults.PreferNSSecureCoding {
    static var bridge: Defaults.NSSecureCodingBridge<Self> { Defaults.NSSecureCodingBridge() }
}

public extension Defaults.Serializable where Self: Codable & RawRepresentable {
    static var bridge: Defaults.RawRepresentableCodableBridge<Self> { Defaults.RawRepresentableCodableBridge() }
}

public extension Defaults.Serializable where Self: Codable & RawRepresentable & Defaults.PreferRawRepresentable {
    static var bridge: Defaults.RawRepresentableBridge<Self> { Defaults.RawRepresentableBridge() }
}

public extension Defaults.Serializable where Self: RawRepresentable {
    static var bridge: Defaults.RawRepresentableBridge<Self> { Defaults.RawRepresentableBridge() }
}

public extension Defaults.Serializable where Self: NSSecureCoding {
    static var bridge: Defaults.NSSecureCodingBridge<Self> { Defaults.NSSecureCodingBridge() }
}

public extension Defaults.CollectionSerializable where Element: Defaults.Serializable {
    static var bridge: Defaults.CollectionBridge<Self> { Defaults.CollectionBridge() }
}

public extension Defaults.SetAlgebraSerializable where Element: Defaults.Serializable & Hashable {
    static var bridge: Defaults.SetAlgebraBridge<Self> { Defaults.SetAlgebraBridge() }
}


================================================
FILE: PaimonMenuBar/Defaults.swift
================================================
//
//  Defaults.swift
//  PaimonMenuBar
//
//  Created by Breezewish on 2022/4/30.
//

import Defaults
import Foundation

extension Defaults.Keys {
    static let uid = Key<String>("uid", default: "")

    static let server = Key<GenshinServer>("server", default: .cn_gf01)

    static let cookie = Key<String>("cookie", default: "")

    // render the icon in the status menu view as template (white icon) or original (colored icon)
    static let isStatusIconTemplate = Key<Bool>("is_status_icon_template", default: true)

    // whether or not to render the text next to the resin icon
    static let isShowResinText = Key<Bool>("is_show_resin_text", default: true)

    // notify on parametric transformer ready
    static let isNotifyParametricReady = Key<Bool>("is_notify_parametric_ready", default: true)

    // store a state of whether the notification has been sent, to avoid duplicated notifications
    static let hasNotifiedParametricReady = Key<Bool>("has_notified_parametric_ready", default: false)

    // update every 2 hours to prevent captchas
    static let recordUpdateInterval = Key<Double>("update_interval", default: 2)

    // if the API encounters a failure (fetch failed, mostly because of the new captcha) ...
    static let fetchFailed = Key<Bool>("fetch_failed", default: false)

    static let lastGameRecord = Key<GameRecord>("game_record", default: GameRecord.empty)
}


================================================
FILE: PaimonMenuBar/GameRecordUpdater.swift
================================================
//
//  GameRecordUpdater.swift
//  PaimonMenuBar
//
//  Created by Spencer Woo on 2022/3/25.
//

import Combine
import Defaults
import Foundation
import Network
import SwiftUI
import UserNotifications

class GameRecordUpdater {
    static let shared = GameRecordUpdater()

    /**
     Fetch latest game record. After finished, UI will be updated accordingly.
     */
    func fetchGameRecordAndRenderNow() async -> GameRecord? {
        if let data = await getGameRecord() {
            DispatchQueue.main.async {
                Defaults[.lastGameRecord] = data
                Defaults[.fetchFailed] = false
            }
            return data
        } else {
            // sendLocalNotification(context: "⚠️ Data fetch failed, check your configuration") {}

            /**
             The new API is more likely to fail due to captcha. Instead of sending notification
             each time fetch fails, we update the UI when we encounter the captcha instead.
             */
            DispatchQueue.main.async {
                Defaults[.fetchFailed] = true
            }
            return nil
        }
    }

    private var lastUpdateAt: DispatchTime = .init(uptimeNanoseconds: 0)
    private var updateTask: Task<Void, Never>?

    /**
     Unlike fetchGameRecordAndRenderNow, this is throttle-protected so that not each call will cause an update.
     Also it will return immediately, schedules an update in the background.

     Must be called in the main thread to avoid race condition.
     **/
    func tryFetchGameRecordAndRender() {
        assert(Thread.isMainThread)

        guard updateTask == nil else {
            // If there is an on-flying request, skip.
            print("Fetch skipped, there is on-flying request")
            return
        }
        let now = DispatchTime.now()
        if now.uptimeNanoseconds - lastUpdateAt.uptimeNanoseconds < 8 * 60 * UInt64(1e9) {
            // If last request is started within 8 minutes, skip.
            print("Fetch skipped, a fetch was performed recently")
            return
        }
        print("Try to update game record now")
        lastUpdateAt = now
        updateTask = Task {
            _ = await fetchGameRecordAndRenderNow()
            updateTask = nil
        }
    }

    /** Must be called in the main thread to avoid race condition. */
    func clearGameRecord() {
        assert(Thread.isMainThread)
        Defaults[.lastGameRecord] = GameRecord.empty
    }

    // MARK: - Self-Update the record when network is actve

    private let networkActivityMon = NWPathMonitor()

    private func startNetworkActivityUpdater() {
        assert(Thread.isMainThread)

        networkActivityMon.pathUpdateHandler = { [weak self] path in
            if path.status != .satisfied {
                return
            }
            print("Network is active")
            self?.tryFetchGameRecordAndRender()
        }
        networkActivityMon.start(queue: DispatchQueue.main)
    }

    // MARK: - Make a request to the remote API and update the record according to the interval

    private var apiUpdateTimer: Timer?

    private func resetApiUpdateTimer() {
        assert(Thread.isMainThread)

        if apiUpdateTimer != nil {
            apiUpdateTimer?.invalidate()
        }
        apiUpdateTimer = Timer
            .scheduledTimer(withTimeInterval: Defaults[.recordUpdateInterval] * 3600, repeats: true) { _ in
                print("Scheduled update is triggered")
                self.tryFetchGameRecordAndRender()
            }

        resetLocalUpdateTimer()
    }

    // MARK: - Self-Update the record according to the interval (which is 8 mins)

    private var localUpdateTimer: Timer?

    private func resetLocalUpdateTimer() {
        assert(Thread.isMainThread)

        if localUpdateTimer != nil {
            localUpdateTimer?.invalidate()
        }
        localUpdateTimer = Timer.scheduledTimer(withTimeInterval: 8 * 60, repeats: true, block: { _ in
            print("Local updater triggered.")

            guard self.updateTask == nil else {
                // If there is an on-flying network request, then skip.
                print("Local updater skipped, there is on-flying request")
                return
            }

            var gameRecord = Defaults[.lastGameRecord]
            guard gameRecord.fetchAt != nil else {
                print("Local updater skipped as there has never been an update from the API")
                return
            }

            // Update resin and recovery time
            if gameRecord.data.current_resin < gameRecord.data.max_resin {
                gameRecord.data.current_resin += 1
            }
            let resinRecoveryTime = Int(gameRecord.data.resin_recovery_time) ?? 0
            if resinRecoveryTime > 0 {
                let updatedTime = resinRecoveryTime - 8 * 60
                gameRecord.data
                    .resin_recovery_time =
                    String(updatedTime > 0 ? updatedTime : 0)
            }

            // Update expedition and their status
            for (index, expedition) in gameRecord.data.expeditions.enumerated() {
                let expeditionRemainedTime = Int(expedition.remained_time) ?? 0
                if expeditionRemainedTime > 0 {
                    let updatedRemainedTime = expeditionRemainedTime - 8 * 60
                    gameRecord.data.expeditions[index]
                        .remained_time = String(updatedRemainedTime > 0 ? updatedRemainedTime : 0)
                    if updatedRemainedTime <= 0 {
                        gameRecord.data.expeditions[index].status = "Finished"
                    }
                }
            }

            Defaults[.lastGameRecord] = gameRecord
        })
    }

    // MARK: - Record update at midnight to avoid today or tomorrow conflicts

    private func setupDayChangeUpdater() {
        assert(Thread.isMainThread)

        NotificationCenter.default.addObserver(forName: .NSCalendarDayChanged, object: nil, queue: .main) { _ in
            print("Day change (midnight) update is triggered")
            self.tryFetchGameRecordAndRender()
        }
    }

    // MARK: - Notification handler

    private func sendLocalNotification(context: LocalizedStringKey, completion: @escaping () -> Void) {
        let center = UNUserNotificationCenter.current()

        center.getNotificationSettings { settings in
            guard settings.authorizationStatus == .authorized || settings.authorizationStatus == .provisional
            else { return }

            let content = UNMutableNotificationContent()
            content.title = String.localized(context)
            content.sound = UNNotificationSound.default

            let request = UNNotificationRequest(
                identifier: UUID().uuidString,
                content: content,
                trigger: UNTimeIntervalNotificationTrigger(timeInterval: 0.1, repeats: false)
            )

            center.add(request) { _ in
                completion()
            }
        }
    }

    // MARK: -

    private var initialized = false

    init() {
        startNetworkActivityUpdater()
        resetApiUpdateTimer()
//        resetLocalUpdateTimer()
        setupDayChangeUpdater()

        Defaults.observe(.recordUpdateInterval) { _ in
            self.onRecordUpdateIntervalChanged()
        }.tieToLifetime(of: self)

        Defaults.observe(.lastGameRecord) { _ in
            self.onGameRecordChanged()
        }.tieToLifetime(of: self)

        initialized = true
    }

    private func onGameRecordChanged() {
        assert(Thread.isMainThread)

        guard initialized else { return }

        print("GameRecord is updated:", Defaults[.lastGameRecord])
        AppDelegate.shared.updateStatusBar()

        // Early return if user chooses not to push notifications
        guard Defaults[.isNotifyParametricReady] else { return }

        let parametricTransformerReady = Defaults[.lastGameRecord].data.transformer.recovery_time.reached

        /**
         Check for parametric transformer ready state - push notification only on:
         1. Parametric transformer ready
         2. User selected to notify when parametric transformer is ready
         3. Notification for parametric transformer ready state has not been sent
         */
        if parametricTransformerReady,
           Defaults[.hasNotifiedParametricReady] == false
        {
            sendLocalNotification(context: "Parametric transformer is ready") {
                // set .hasNotifiedParametricReady to true on notification delivery
                Defaults[.hasNotifiedParametricReady] = true
            }
        }

        // If parametric transformer is not ready but .hasNotifiedParametricReady is true, then reset the trigger
        if parametricTransformerReady == false,
           Defaults[.hasNotifiedParametricReady]
        {
            Defaults[.hasNotifiedParametricReady] = false
        }
    }

    private func onRecordUpdateIntervalChanged() {
        assert(Thread.isMainThread)

        guard initialized else { return }

        print("RecordUpdateInterval is changed: ", Defaults[.recordUpdateInterval])
        resetApiUpdateTimer()
    }
}


================================================
FILE: PaimonMenuBar/Info.plist
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>SUEnableInstallerLauncherService</key>
	<true/>
	<key>SUFeedURL</key>
	<string>https://raw.githubusercontent.com/spencerwooo/PaimonMenuBar/main/appcast.xml</string>
	<key>SUPublicEDKey</key>
	<string>AhJieofGr0gFEypC7KPkg3f37YZ2Pj9lo2+reSx1a20=</string>
</dict>
</plist>


================================================
FILE: PaimonMenuBar/MenuExtrasView.swift
================================================
//
//  MenuView.swift
//  PaimonMenuBar
//
//  Created by Spencer Woo on 2022/3/25.
//

import Defaults
import Foundation
import Kingfisher
import SwiftUI

class RelativeFormatter {
    private let df = DateFormatter()

    init() {
        df.dateStyle = DateFormatter.Style.long
        df.timeStyle = DateFormatter.Style.short
        df.doesRelativeDateFormatting = true
    }

    func string(time: Date) -> String {
        return df.string(from: time)
    }
}

/// Return the formatted time interval in a human-readable string
/// - Parameter timeInterval: A time interval represented in seconds
/// - Returns: A human-readable string representing the time interval
private func formatTimeInterval(timeInterval: String) -> String {
    let formatter = DateComponentsFormatter()
    formatter.allowedUnits = [.hour, .minute]
    formatter.unitsStyle = .abbreviated
    return formatter.string(from: (TimeInterval(timeInterval) ?? TimeInterval("0"))!) ?? ""
}

/// Format a date that is of 'timeInterval' seconds away from now
/// - Parameter timeInterval: The number of seconds away from current time
/// - Returns: A human-readable string describing the future date
private func formatFutureDate(timeInterval: String) -> String {
    let currentTime = Date()
    let futureTime = currentTime.addingTimeInterval((TimeInterval(timeInterval) ?? TimeInterval("0"))!)

    if Calendar.current.isDateInToday(futureTime) {
        return "\(String.localized("Today")) \(futureTime.shortenedFormatted)"
    }
    if Calendar.current.isDateInTomorrow(futureTime) {
        return "\(String.localized("Tomorrow")) \(futureTime.shortenedFormatted)"
    }
    // This should not happen, but just in case.
    return futureTime.defaultFormatted
}

struct MenuExtrasView: View {
    @Default(.lastGameRecord) private var lastGameRecord

    var body: some View {
        ZStack {
            VStack(spacing: 8) {
                ResinView(
                    currentResin: lastGameRecord.data.current_resin,
                    maxResin: lastGameRecord.data.max_resin,
                    resinRecoveryTime: lastGameRecord.data.resin_recovery_time,
                    fetchAt: lastGameRecord.fetchAt
                )

                ExpeditionView(
                    expeditions: lastGameRecord.data.expeditions,
                    maxExpeditionNum: lastGameRecord.data.max_expedition_num,
                    currentExpeditionNum: lastGameRecord.data.current_expedition_num
                )

                DailyCommissionView(
                    finishedTaskNum: lastGameRecord.data.finished_task_num,
                    totalTaskNum: lastGameRecord.data.total_task_num
                )

                ExtraTaskRewardView(
                    remainResinDiscountNum: lastGameRecord.data.remain_resin_discount_num,
                    resinDiscountNumLimit: lastGameRecord.data.resin_discount_num_limit,
                    isExtraTaskRewardReceived: lastGameRecord.data.is_extra_task_reward_received
                )

                HomeCoinView(
                    currentHomeCoin: lastGameRecord.data.current_home_coin,
                    maxHomeCoin: lastGameRecord.data.max_home_coin,
                    homeCoinRecoveryTime: lastGameRecord.data.home_coin_recovery_time
                )

                ParametricTransformerView(transformer: lastGameRecord.data.transformer)
            }
            .padding([.horizontal])
            .padding([.vertical], 8)
            .opacity(lastGameRecord.fetchAt == nil ? 0.4 : 1.0)
            .blur(radius: lastGameRecord.fetchAt == nil ? 4 : 0)

            if lastGameRecord.fetchAt == nil {
                VStack(spacing: 16) {
                    Spacer()

                    Image("KokomiSad").resizable().frame(width: 72, height: 72)
                    Text("NOT INITIALIZED")
                        .font(.custom("Avenir Next Bold", size: 24, relativeTo: .largeTitle).italic())
                    Text(
                        "Please go to _Preferences > Configuration_ and setup your in-game **_UID_**, **_server_**, and **_cookie_**."
                    )

                    Spacer()
                    HStack {
                        Label("Open preferences here", systemImage: "arrow.down")
                        Spacer()
                        Link(destination: URL(string: "https://paimon.swo.moe/#how-to-get-my-cookie")!) {
                            Button("?") {
                                print("Navigating to help page.")
                            }.clipShape(Circle()).shadow(radius: 1)
                        }
                    }
                }
                .padding()
            }
        }.onAppear {
            GameRecordUpdater.shared.tryFetchGameRecordAndRender()
        }
    }
}

struct ResinView: View {
    @Default(.fetchFailed) private var fetchFailed

    let currentResin: Int
    let maxResin: Int
    let resinRecoveryTime: String
    let fetchAt: Date?

    private let formatter = RelativeFormatter()

    var body: some View {
        VStack(spacing: 8) {
            HStack {
                Label {
                    Text((fetchAt == nil) ? "Not updated" :
                        (fetchFailed ? "Update failed, check the official APP for captchas" :
                            "Update: \(formatter.string(time: fetchAt!))"))
                        .font(.caption)
                } icon: {
                    Image(systemName: "circle.fill").scaleEffect(0.4).frame(width: 5)
                        .foregroundColor(fetchFailed ? Color.red : Color.green)
                }.opacity(0.6)
            }

            HStack(spacing: 4) {
                Image("FragileResin")
                    .resizable()
                    .frame(width: 16, height: 16)
                Text("Current Resin")
                    .font(.subheadline)
                    .opacity(0.6)
                Spacer()
            }

            Text("\(currentResin)/\(maxResin)")
                .frame(maxWidth: .infinity, alignment: .leading)
                .font(.custom("Avenir Next Bold", size: 25, relativeTo: .largeTitle).italic())

            HStack {
                Label("Fully replenished", systemImage: "moon.circle")
                Spacer()
                Text(formatTimeInterval(timeInterval: resinRecoveryTime))
                    .font(.custom("Avenir Next Demi Bold", size: 13, relativeTo: .body).italic())
            }
            HStack {
                Label("ETA", systemImage: "clock")
                Spacer()
                Text(formatFutureDate(timeInterval: resinRecoveryTime))
                    .font(.custom("Avenir Next Demi Bold", size: 13, relativeTo: .body).italic())
            }
            Divider()
        }
    }
}

struct ExpeditionView: View {
    let expeditions: [Expeditions]
    let maxExpeditionNum: Int
    let currentExpeditionNum: Int

    var body: some View {
        VStack(spacing: 10) {
            HStack {
                Text("Expeditions \(currentExpeditionNum)/\(maxExpeditionNum)")
                    .font(.subheadline)
                    .opacity(0.6)
                Spacer()
                Image("Expedition").resizable().frame(width: 18, height: 18)
            }

            ForEach(expeditions, id: \.self) { expedition in
                ExpeditionItemView(
                    status: expedition.status, avatar: expedition.avatar_side_icon,
                    remainedTime: expedition.remained_time
                )
            }

            Divider()
        }
    }
}

struct ExpeditionItemView: View {
    let status: String
    let avatar: String
    let remainedTime: String

    // 20 hours in seconds
    let totalExpeditionTime: Float = 20 * 60 * 60

    var body: some View {
        HStack {
            KFImage.url(URL(string: avatar))
                .resizable()
                .placeholder { Color.gray.opacity(0.3) }
                .clipShape(Circle())
                .overlay(Circle().stroke(status == "Finished" ? Color.green : Color.gray))
                .frame(width: 22, height: 22)

            VStack(spacing: 6) {
                HStack {
                    Text(status == "Finished" ? String.localized("Complete") : String.localized("Exploring"))
                    Spacer()
                    Text(formatTimeInterval(timeInterval: remainedTime))
                        .font(.custom("Avenir Next Demi Bold", size: 13, relativeTo: .body).italic())
                }

                ProgressView(value: totalExpeditionTime - (Float(remainedTime) ?? 0), total: totalExpeditionTime)
                    .progressViewStyle(LinearProgressViewStyle(tint: status == "Finished" ?
                            Color.green : Color(red: 0.89, green: 0.90, blue: 0.92)))
                    .frame(height: 1).scaleEffect(x: 1, y: 0.4, anchor: .center)
            }
        }
    }
}

struct DailyCommissionView: View {
    let finishedTaskNum: Int
    let totalTaskNum: Int

    var body: some View {
        HStack {
            Image("Commision")
                .resizable()
                .frame(width: 20, height: 20, alignment: .leading)
            Text("Daily commissions")
            Spacer()
            Text("\(totalTaskNum - finishedTaskNum) left")
                .font(.custom("Avenir Next Demi Bold", size: 13, relativeTo: .body).italic())
        }
    }
}

struct HomeCoinView: View {
    let currentHomeCoin: Int
    let maxHomeCoin: Int
    let homeCoinRecoveryTime: String

    var body: some View {
        HStack {
            Image("JarOfRiches")
                .resizable()
                .scaledToFit()
                .frame(width: 20, height: 20, alignment: .center)
            Text("Realm currency")
            Spacer()
            Text("\(currentHomeCoin)/\(maxHomeCoin)")
                .font(.custom("Avenir Next Demi Bold", size: 13, relativeTo: .body).italic())
        }
    }
}

struct ExtraTaskRewardView: View {
    let remainResinDiscountNum: Int
    let resinDiscountNumLimit: Int
    let isExtraTaskRewardReceived: Bool

    var body: some View {
        HStack {
            Image("Domain")
                .resizable()
                .scaledToFit()
                .frame(width: 20, height: 20, alignment: .center)
            Text("Weekly bosses")
            Spacer()
            Text("\(remainResinDiscountNum) left")
                .font(.custom("Avenir Next Demi Bold", size: 13, relativeTo: .body).italic())
        }
    }
}

struct ParametricTransformerView: View {
    let transformer: Transformer

    func formatRecoveryTime(recoveryTime: RecoveryTime) -> String {
        if recoveryTime.reached {
            return "Ready"
        } else {
            return recoveryTime
                .Day != 0 ? "\(recoveryTime.Day) \(String.localized(recoveryTime.Day == 1 ? "day" : "days"))" :
                recoveryTime
                .Hour != 0 ? "\(recoveryTime.Hour) \(String.localized(recoveryTime.Hour == 1 ? "hour" : "hours"))"
                : "\(recoveryTime.Minute) \(String.localized(recoveryTime.Minute == 1 ? "min" : "mins"))"
        }
    }

    var body: some View {
        HStack {
            Image("ParametricTransformer")
                .resizable()
                .scaledToFit()
                .frame(width: 20, height: 20, alignment: .center)
            Text("Parametric transformer")
            Spacer()
            Text(transformer.obtained ? formatRecoveryTime(recoveryTime: transformer.recovery_time) : "Unavailable")
                .font(.custom("Avenir Next Demi Bold", size: 13, relativeTo: .body).italic())
        }
    }
}

struct MenuView_Previews: PreviewProvider {
    static var previews: some View {
        MenuExtrasView()
            .frame(width: 290.0)
            .frame(height: 472.0)
    }
}


================================================
FILE: PaimonMenuBar/Networking.swift
================================================
//
//  Networking.swift
//  PaimonMenuBar
//
//  Created by Spencer Woo on 2022/3/24.
//

import CryptoKit
import Defaults
import Foundation

extension String {
    // MD5 hash from: https://powermanuscript.medium.com/swift-5-2-macos-md5-hash-for-some-simple-use-cases-66be9e274182
    var MD5: String {
        let computed = Insecure.MD5.hash(data: data(using: .utf8)!)
        return computed.map { String(format: "%02hhx", $0) }.joined()
    }
}

func getDS(uid: String, server: GenshinServer) -> String {
    // Part 1: current unix timestamp
    let timestamp = Int(Date().timeIntervalSince1970)

    // Part 2: a random integer from 100,000 to 200,000
    let randomString = Int.random(in: 100_000 ..< 200_000)

    // Part 3: MD5 hash of salt
    let salt = server.isCNServer ? "xV8v4Qu54lUKrEYFZkJhB8cuOh9Asafs" : "okr4obncj8bw5a65hbnn5oo6ixjc3l9w"
    let sign = "salt=\(salt)&t=\(timestamp)&r=\(randomString)&b=&q=role_id=\(uid)&server=\(server)".MD5

    return "\(timestamp),\(randomString),\(sign)"
}

func getGameRecord() async -> GameRecord? {
    // API to query Genshin game record
    let apiCn = "https://api-takumi-record.mihoyo.com/game_record/app/genshin/api/dailyNote"
    let apiGlobal = "https://bbs-api-os.hoyolab.com/game_record/app/genshin/api/dailyNote"

    let uid = Defaults[.uid]
    let server = Defaults[.server]
    let cookie = Defaults[.cookie]
    guard !uid.isEmpty, !cookie.isEmpty else {
        print("Fetch skipped, because cookie is not set")
        return nil
    }

    print("Fetching game record data...", uid, server)

    let api = server.isCNServer ? apiCn : apiGlobal
    guard let url = URL(string: "\(api)?role_id=\(uid)&server=\(server)") else { return nil }

    // Reverse engineering Mihoyo API ;)
    let DS = getDS(uid: uid, server: server)
    let appVersion = server.isCNServer ? "2.26.1" : "2.9.1"
    let clientType = server.isCNServer ? "5" : "2"

    // Construct request with query parameters and relevant headers
    var req = URLRequest(url: url)
    req.httpMethod = "GET"

    // Add required headers
    req.setValue(cookie, forHTTPHeaderField: "Cookie")
    req.setValue(DS, forHTTPHeaderField: "DS")
    req.setValue(appVersion, forHTTPHeaderField: "x-rpc-app_version")
    req.setValue(clientType, forHTTPHeaderField: "x-rpc-client_type")

    // Perform HTTP request
    do {
        let (data, _) = try await URLSession.shared.data(for: req)
//        if let string = String(bytes: data, encoding: .utf8) {
//            print(string)
//        }
        var gameRecord = try? JSONDecoder().decode(GameRecord.self, from: data)
        gameRecord?.fetchAt = Date()
        return gameRecord
    } catch {
        print("Invalid data")
        return nil
    }
}


================================================
FILE: PaimonMenuBar/PaimonMenuBar.entitlements
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>com.apple.security.app-sandbox</key>
	<true/>
	<key>com.apple.security.files.user-selected.read-only</key>
	<true/>
	<key>com.apple.security.network.client</key>
	<true/>
	<key>com.apple.security.temporary-exception.mach-lookup.global-name</key>
	<array>
		<string>$(PRODUCT_BUNDLE_IDENTIFIER)-spks</string>
		<string>$(PRODUCT_BUNDLE_IDENTIFIER)-spki</string>
	</array>
</dict>
</plist>


================================================
FILE: PaimonMenuBar/PaimonMenuBarApp.swift
================================================
//
//  PaimonMenuBarApp.swift
//  PaimonMenuBar
//
//  Created by Spencer Woo on 2022/3/23.
//

import SwiftUI

@main
struct PaimonMenuBarApp: App {
    @NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate

    var body: some Scene {
        WindowGroup {
            if false {}
        }
        .commands {
            CommandGroup(after: .appInfo) {
                CheckForUpdatesView(updaterViewModel: UpdaterViewModel.shared)
            }
        }

        Settings {
            SettingsView()
        }
    }
}


================================================
FILE: PaimonMenuBar/Preview Content/Preview Assets.xcassets/Contents.json
================================================
{
  "info" : {
    "author" : "xcode",
    "version" : 1
  }
}


================================================
FILE: PaimonMenuBar/SettingsView.swift
================================================
//
//  ContentView.swift
//  PaimonMenuBar
//
//  Created by Spencer Woo on 2022/3/23.
//

import Defaults
import LaunchAtLogin
import SwiftUI
import UserNotifications

// This additional view is needed for the disabled state on the menu item to work properly before Monterey.
// See https://stackoverflow.com/questions/68553092/menu-not-updating-swiftui-bug for more information
struct CheckForUpdatesView: View {
    @ObservedObject var updaterViewModel: UpdaterViewModel

    var body: some View {
        Button("Check for Updates", action: updaterViewModel.checkForUpdates)
            .disabled(!updaterViewModel.canCheckForUpdates)
    }
}

func getNotificationPermission(completion: @escaping (Bool) -> Void) {
    UNUserNotificationCenter.current().getNotificationSettings { settings in
        completion(settings.authorizationStatus == .authorized || settings.authorizationStatus == .provisional)
    }
}

struct PreferenceSettingsView: View {
    @Default(.recordUpdateInterval) private var recordUpdateInterval
    @Default(.isStatusIconTemplate) private var isStatusIconTemplate
    @Default(.isShowResinText) private var isShowResinText
    @Default(.isNotifyParametricReady) private var isNotifyParametricReady
    @Default(.lastGameRecord) private var lastGameRecord

    @StateObject var updaterViewModel = UpdaterViewModel.shared

    @State private var isEditing = false
    @State private var isNotNotificationAuthorized = false

    var body: some View {
        VStack(spacing: 20) {
            Form {
                LaunchAtLogin.Toggle {
                    Text("Launch at Login")
                }
                .formLabel(Text("Start:"))

                CheckForUpdatesView(updaterViewModel: updaterViewModel)
                    .formLabel(Text("Updates:"))
                Text("Current version: \(Bundle.main.appVersion ?? "") (\(Bundle.main.buildNumber ?? ""))")
                    .font(.caption).opacity(0.6)

                HStack {
                    Defaults.Toggle(key: .isStatusIconTemplate) {
                        Image("FragileResin")
                            .renderingMode(isStatusIconTemplate ? .template : .original)
                            .frame(width: 19, height: 19)
                    }
                    .onChange { _ in
                        AppDelegate.shared.updateStatusIcon()
                    }

                    Defaults.Toggle(key: .isShowResinText) {
                        Text("\(lastGameRecord.data.current_resin)/\(lastGameRecord.data.max_resin)")
                            .opacity(isShowResinText ? 1 : 0.4)
                    }
                    .onChange { _ in
                        AppDelegate.shared.updateStatusButtonTitle()
                    }
                }
                .formLabel(Text("Menubar icon:"))

                HStack(spacing: 2) {
                    Text(isStatusIconTemplate ? "Native macOS adaptive icon." : "Colored icon.").font(.caption)
                        .opacity(0.6)
                    Text(isShowResinText ? "With resin counter." : "Resin counter hidden.").font(.caption)
                        .opacity(0.6)
                }

                Defaults.Toggle(key: .isNotifyParametricReady) {
                    Image(
                        systemName: isNotifyParametricReady ? "bell.badge" : "bell.slash"
                    )
                    if isNotNotificationAuthorized {
                        Label("⚠️ Notification unauthorized.", systemImage: "arrow.left")
                            .font(.caption2).opacity(0.8)
                    }
                }
                .formLabel(Text("Notify:"))
                .onAppear {
                    getNotificationPermission { authorized in
                        isNotNotificationAuthorized = !authorized
                    }
                }
                .onChange(of: isNotifyParametricReady) { _ in
                    getNotificationPermission { authorized in
                        isNotNotificationAuthorized = !authorized
                    }
                }
                Text("... when parametric transformer is ready.").font(.caption).opacity(0.6)

                Slider(value: $recordUpdateInterval, in: 1 ... 6, step: 1, label: {
                    Text("Update interval:")
                }) { editing in
                    isEditing = editing
                }
                .frame(width: 360)

                Text("Paimon fetches data every \(recordUpdateInterval, specifier: "%.0f") hour(s)*")
                    .font(.caption).opacity(0.6)
            }

            Divider()

            Label(
                "*Updating every 1-3 hours is sufficient, to prevent from being captcha-ed. Don't worry, as Paimon will auto-update the data offline as time passes.",
                image: "FragileResin"
            )
            .font(.caption).opacity(0.6).frame(width: 400)
        }
    }
}

struct ConfigurationSettingsView: View {
    @Default(.uid) private var uid
    @Default(.server) private var server
    @Default(.cookie) private var cookie

    @State private var alertText = ""
    @State private var alertMessage = ""
    @State private var showConfigValidAlert = false

    @State private var isLoading = false

    var body: some View {
        VStack {
            Text("User")
                .font(.headline)
                .frame(maxWidth: .infinity, alignment: .leading)
            Form {
                TextField("UID:", text: $uid)
                    .textFieldStyle(.roundedBorder)
                Picker("Server:", selection: $server) {
                    ForEach(GenshinServer.allCases, id: \.id) { value in
                        Text(value.serverName).tag(value)
                    }
                }.pickerStyle(SegmentedPickerStyle())
            }.padding([.bottom])

            Text("Cookie")
                .font(.headline)
                .frame(maxWidth: .infinity, alignment: .leading)
            HStack {
                Text("Paste your cookie from:")
                    .font(.subheadline)
                Link(destination: URL(string: server.cookieSiteUrl)!) {
                    Text(server.cookieSiteUrl)
                        .font(.subheadline)
                }
            }
            .frame(maxWidth: .infinity, alignment: .leading)
            TextEditor(text: $cookie)
                .font(.system(.body, design: .monospaced))
                .frame(height: 120).cornerRadius(6)
                .background(Color.black.cornerRadius(6).shadow(radius: 0.5, y: 0.8).opacity(0.6))

            Spacer()

            HStack {
                Label("This cookie is only stored locally.", systemImage: "exclamationmark.circle")
                    .font(.caption).opacity(0.6)
                Spacer()
                Button {
                    GameRecordUpdater.shared.clearGameRecord()
                    Task {
                        isLoading = true
                        if let _ = await GameRecordUpdater.shared.fetchGameRecordAndRenderNow() {
                            self.alertText = String.localized("👌 It's working!")
                            self.alertMessage = String.localized("Your config is valid.")
                        } else {
                            self.alertText = String.localized("🚫 Whoooops...")
                            self.alertMessage = String.localized("Failed to fetch, check your config.")
                        }
                        self.showConfigValidAlert.toggle()
                        isLoading = false
                    }
                } label: {
                    Label {
                        Text("Test config")
                    } icon: {
                        Image(systemName: "bolt")
                            .opacity(isLoading ? 0 : 1)
                            .overlay(isLoading ? ProgressView().scaleEffect(0.4) : nil)
                    }
                }
                .alert(isPresented: self.$showConfigValidAlert, content: {
                    Alert(title: Text(alertText), message: Text(alertMessage))
                })
                .disabled(isLoading)

                Link(destination: URL(string: "https://paimon.swo.moe/#how-to-get-my-cookie")!) {
                    Button("?") {
                        print("Navigating to help page.")
                    }.clipShape(Circle()).shadow(radius: 1)
                }
            }

        }.padding()
    }
}

struct AboutSettingsView: View {
    var body: some View {
        VStack(spacing: 8) {
            Image(nsImage: NSImage(named: "AppIcon") ?? NSImage())
            Text(Bundle.main.appName ?? "").font(.headline.bold())
            Text("Build \(Bundle.main.appVersion ?? "") (\(Bundle.main.buildNumber ?? ""))")
                .font(.system(.subheadline, design: .monospaced))

            Divider()

            Text(
                "Made with love @ [SpencerWoo](https://spencerwoo.com) | Check [Paimon's website](https://paimon.swo.moe)"
            )
            .font(.system(.caption, design: .monospaced))
            Text(
                "Icon by [Chawong](https://www.pixiv.net/en/artworks/92415888) | GitHub: [spencerwooo/PaimonMenuBar](https://github.com/spencerwooo/PaimonMenuBar)"
            )
            .font(.system(.caption, design: .monospaced))
        }
    }
}

struct SettingsView: View {
    var body: some View {
        TabView {
            PreferenceSettingsView()
                .tabItem {
                    Label("Preferences", systemImage: "gear")
                }
            ConfigurationSettingsView()
                .tabItem {
                    Label("Configuration", systemImage: "square.and.pencil")
                }
            AboutSettingsView()
                .tabItem {
                    Label("About", systemImage: "person")
                }
        }
        .frame(width: 560, height: 360)
    }
}

/// Alignment guide for aligning a text field in a `Form`.
/// Thanks for Jim Dovey  https://developer.apple.com/forums/thread/126268
extension HorizontalAlignment {
    private enum ControlAlignment: AlignmentID {
        static func defaultValue(in context: ViewDimensions) -> CGFloat {
            return context[HorizontalAlignment.center]
        }
    }

    static let controlAlignment = HorizontalAlignment(ControlAlignment.self)
}

// Adapted from https://gist.github.com/marcprux/afd2f80baa5b6d60865182a828e83586
public extension View {
    /// Attaches a label to this view for laying out in a `Form`
    /// - Parameter view: the label view to use
    /// - Returns: an `HStack` with an alignment guide for placing in a form
    func formLabel<V: View>(_ view: V) -> some View {
        HStack {
            view
            self
                .alignmentGuide(.controlAlignment) { $0[.leading] }
        }
        .alignmentGuide(.leading) { $0[.controlAlignment] }
    }
}

struct SettingsView_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            SettingsView()
            PreferenceSettingsView()
            ConfigurationSettingsView()
            AboutSettingsView()
        }
    }
}


================================================
FILE: PaimonMenuBar/UpdaterViewModel.swift
================================================
//
//  UpdaterViewModel.swift
//  PaimonMenuBar
//
//  Created by Spencer Woo on 2022/3/31.
//

import Foundation
import Sparkle

final class UpdaterViewModel: ObservableObject {
    private let updaterController: SPUStandardUpdaterController
    static let shared = UpdaterViewModel()

    @Published var canCheckForUpdates = false

    init() {
        updaterController = SPUStandardUpdaterController(
            startingUpdater: true,
            updaterDelegate: nil,
            userDriverDelegate: nil
        )

        updaterController.updater.publisher(for: \.canCheckForUpdates)
            .assign(to: &$canCheckForUpdates)
    }

    func checkForUpdates() {
        updaterController.checkForUpdates(nil)
    }
}


================================================
FILE: PaimonMenuBar/en.lproj/Localizable.strings
================================================



================================================
FILE: PaimonMenuBar/zh-Hans.lproj/Localizable.strings
================================================
"*Updating every 1-3 hours is sufficient, to prevent from being captcha-ed. Don't worry, as Paimon will auto-update the data offline as time passes." = "* 为了避免触发验证码,建议最短间隔 1 至 3 小时更新一次数据。不必担心,派蒙会自动在本地随着时间流逝更新数据。";

"About" = "关于";

"Check for Updates" = "检查更新";

"Complete" = "完成";

"Configuration" = "配置信息";

"Current Resin" = "当前树脂";

"Current version: %@ (%@)" = "当前版本:%@ (%@)";

"Daily commissions" = "每日委托";

"ETA" = "全部恢复于";

"Expeditions %lld/%lld" = "探索派遣 %lld/%lld";

"Fully replenished" = "距离全部恢复";

"Launch at Login" = "登录时启动";

"Paimon fetches data every %.0f hour(s)*" = "派蒙每隔 %.0f 小时更新一次数据 *";

"Paste your cookie from:" = "从这里粘贴你的 Cookie:";

"User" = "个人信息";

"Preferences" = "偏好设置";

"Realm currency" = "洞天宝钱";

"UID:" = "游戏内 UID:";

"Server:" = "服务器:";

"Start:" = "启动:";

"Test config" = "测试配置";

"Update interval:" = "更新频率:";

"Updates:" = "更新:";

"Check for Updates" = "检查更新";

"Tomorrow" = "明日";

"Today" = "今日";

"Exploring" = "剩余探索时间";

"Complete" = "探索完成";

"Weekly bosses" = "半价周本";

"Quit" = "退出";

"👌 It's working!" = "👌 成功!";

"Your config is valid." = "你的配置信息正确无误。";

"🚫 Whoooops..." = "🚫 啊哦……";

"Failed to fetch, check your config." = "数据获取失败,请检查配置。";

"Parametric transformer" = "参量质变仪";

"Update: %@" = "更新时间:%@";

"Not updated" = "未更新";

"Update failed, check the official APP for captchas" = "更新失败:检查官方应用中是否触发验证码";

"Menubar icon:" = "菜单栏图标:";

"Native macOS adaptive icon." = "原生 macOS 自适应图标。";

"Colored icon." = "彩色图标。";

"With resin counter." = "显示树脂。";

"Resin counter hidden." = "树脂隐藏。";

"%@ left" = "剩 %@ 个";

"Ready" = "冷却完成";

"Unavailable" = "未解锁";

"day" = "天";

"days" = "天";

"hour" = "时";

"hours" = "时";

"min" = "分";

"mins" = "分";

"NOT INITIALIZED" = "尚未配置";

"Please go to _Preferences > Configuration_ and setup your in-game **_UID_**, **_server_**, and **_cookie_**." = "请前往「偏好设置 > 配置信息」设置你的游戏内 **_UID_**、**_服务器_**、以及 **_米游社 cookie_**.";

"Open preferences here" = "在这里打开偏好设置";

"Parametric transformer is ready" = "参量质变仪已就绪";

"Notify:" = "通知:";

"... when parametric transformer is ready." = "… 当参量质变仪就绪时。";

"⚠️ Data fetch failed, check your configuration" = "⚠️ 数据获取失败,检查你的配置信息";

"⚠️ Notification unauthorized." = "⚠️ 没有推送通知权限。";

"This cookie is only stored locally." = "Cookie 仅保存于本地";


================================================
FILE: PaimonMenuBar.xcodeproj/project.pbxproj
================================================
// !$*UTF8*$!
{
	archiveVersion = 1;
	classes = {
	};
	objectVersion = 55;
	objects = {

/* Begin PBXBuildFile section */
		320C466128254F8700C6932E /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 320C466028254F8700C6932E /* Kingfisher */; };
		323850E1282540EE0097B5C2 /* Compatibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 323850E0282540EE0097B5C2 /* Compatibility.swift */; };
		76085E6127FC23B000960915 /* LaunchAtLogin in Frameworks */ = {isa = PBXBuildFile; productRef = 76085E6027FC23B000960915 /* LaunchAtLogin */; };
		76085E6427FC23EA00960915 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = 76085E6327FC23EA00960915 /* Sparkle */; };
		7621675327F2FC080023F8B2 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 7621675527F2FC080023F8B2 /* Localizable.strings */; };
		7686474127EF082400BCC350 /* Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7686474027EF082400BCC350 /* Bundle.swift */; };
		76C290F027EAFFB000A30C9F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76C290EF27EAFFB000A30C9F /* AppDelegate.swift */; };
		76CCDDDE27EAD1C4009CFC64 /* PaimonMenuBarApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76CCDDDD27EAD1C4009CFC64 /* PaimonMenuBarApp.swift */; };
		76CCDDE027EAD1C4009CFC64 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76CCDDDF27EAD1C4009CFC64 /* SettingsView.swift */; };
		76CCDDE227EAD1C5009CFC64 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 76CCDDE127EAD1C5009CFC64 /* Assets.xcassets */; };
		76CCDDE527EAD1C5009CFC64 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 76CCDDE427EAD1C5009CFC64 /* Preview Assets.xcassets */; };
		76D73BBF27EC650500CCDEA6 /* DataModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76D73BBE27EC650500CCDEA6 /* DataModels.swift */; };
		76D73BC127EC67D300CCDEA6 /* Networking.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76D73BC027EC67D300CCDEA6 /* Networking.swift */; };
		76E429A927EDDE000032313C /* GameRecordUpdater.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76E429A827EDDE000032313C /* GameRecordUpdater.swift */; };
		76E986B627EDD5FC004ECC6C /* MenuExtrasView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76E986B527EDD5FC004ECC6C /* MenuExtrasView.swift */; };
		76F9AE6D27F570D90051CDC8 /* UpdaterViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76F9AE6C27F570D90051CDC8 /* UpdaterViewModel.swift */; };
		9F38F96F281D1BE90004D240 /* Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F38F96E281D1BE90004D240 /* Defaults.swift */; };
		9F38F972281D4D000004D240 /* Defaults in Frameworks */ = {isa = PBXBuildFile; productRef = 9F38F971281D4D000004D240 /* Defaults */; };
		9F38F974281D4D120004D240 /* Defaults+Workaround.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F38F973281D4D120004D240 /* Defaults+Workaround.swift */; };
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
		323850E0282540EE0097B5C2 /* Compatibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Compatibility.swift; sourceTree = "<group>"; };
		7621675427F2FC080023F8B2 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
		7621675627F2FC0B0023F8B2 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = "<group>"; };
		7686474027EF082400BCC350 /* Bundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bundle.swift; sourceTree = "<group>"; };
		76B3F03127F2B76100833555 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
		76C290EF27EAFFB000A30C9F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
		76CCDDDD27EAD1C4009CFC64 /* PaimonMenuBarApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaimonMenuBarApp.swift; sourceTree = "<group>"; };
		76CCDDDF27EAD1C4009CFC64 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
		76CCDDE127EAD1C5009CFC64 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
		76CCDDE427EAD1C5009CFC64 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
		76CCDDE627EAD1C5009CFC64 /* PaimonMenuBar.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = PaimonMenuBar.entitlements; sourceTree = "<group>"; };
		76D73BBE27EC650500CCDEA6 /* DataModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataModels.swift; sourceTree = "<group>"; };
		76D73BC027EC67D300CCDEA6 /* Networking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Networking.swift; sourceTree = "<group>"; };
		76E429A827EDDE000032313C /* GameRecordUpdater.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameRecordUpdater.swift; sourceTree = "<group>"; };
		76E986B527EDD5FC004ECC6C /* MenuExtrasView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuExtrasView.swift; sourceTree = "<group>"; };
		76F9AE6B27F570640051CDC8 /* PaimonMenuBar.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PaimonMenuBar.app; sourceTree = BUILT_PRODUCTS_DIR; };
		76F9AE6C27F570D90051CDC8 /* UpdaterViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdaterViewModel.swift; sourceTree = "<group>"; };
		9F38F96E281D1BE90004D240 /* Defaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Defaults.swift; sourceTree = "<group>"; };
		9F38F973281D4D120004D240 /* Defaults+Workaround.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Defaults+Workaround.swift"; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
		76CCDDD727EAD1C4009CFC64 /* Frameworks */ = {
			isa = PBXFrameworksBuildPhase;
			buildActionMask = 2147483647;
			files = (
				320C466128254F8700C6932E /* Kingfisher in Frameworks */,
				9F38F972281D4D000004D240 /* Defaults in Frameworks */,
				76085E6127FC23B000960915 /* LaunchAtLogin in Frameworks */,
				76085E6427FC23EA00960915 /* Sparkle in Frameworks */,
			);
			runOnlyForDeploymentPostprocessing = 0;
		};
/* End PBXFrameworksBuildPhase section */

/* Begin PBXGroup section */
		76CCDDD127EAD1C4009CFC64 = {
			isa = PBXGroup;
			children = (
				76B3F03127F2B76100833555 /* README.md */,
				76CCDDDC27EAD1C4009CFC64 /* PaimonMenuBar */,
				76F9AE6B27F570640051CDC8 /* PaimonMenuBar.app */,
			);
			sourceTree = "<group>";
		};
		76CCDDDC27EAD1C4009CFC64 /* PaimonMenuBar */ = {
			isa = PBXGroup;
			children = (
				7621675527F2FC080023F8B2 /* Localizable.strings */,
				76CCDDDD27EAD1C4009CFC64 /* PaimonMenuBarApp.swift */,
				76E986B527EDD5FC004ECC6C /* MenuExtrasView.swift */,
				76E429A827EDDE000032313C /* GameRecordUpdater.swift */,
				76CCDDDF27EAD1C4009CFC64 /* SettingsView.swift */,
				76F9AE6C27F570D90051CDC8 /* UpdaterViewModel.swift */,
				9F38F96E281D1BE90004D240 /* Defaults.swift */,
				7686474027EF082400BCC350 /* Bundle.swift */,
				76CCDDE127EAD1C5009CFC64 /* Assets.xcassets */,
				76CCDDE627EAD1C5009CFC64 /* PaimonMenuBar.entitlements */,
				76CCDDE327EAD1C5009CFC64 /* Preview Content */,
				76C290EF27EAFFB000A30C9F /* AppDelegate.swift */,
				76D73BBE27EC650500CCDEA6 /* DataModels.swift */,
				76D73BC027EC67D300CCDEA6 /* Networking.swift */,
				9F38F973281D4D120004D240 /* Defaults+Workaround.swift */,
				323850E0282540EE0097B5C2 /* Compatibility.swift */,
			);
			path = PaimonMenuBar;
			sourceTree = "<group>";
		};
		76CCDDE327EAD1C5009CFC64 /* Preview Content */ = {
			isa = PBXGroup;
			children = (
				76CCDDE427EAD1C5009CFC64 /* Preview Assets.xcassets */,
			);
			path = "Preview Content";
			sourceTree = "<group>";
		};
/* End PBXGroup section */

/* Begin PBXNativeTarget section */
		76CCDDD927EAD1C4009CFC64 /* PaimonMenuBar */ = {
			isa = PBXNativeTarget;
			buildConfigurationList = 76CCDDE927EAD1C5009CFC64 /* Build configuration list for PBXNativeTarget "PaimonMenuBar" */;
			buildPhases = (
				76CCDDD627EAD1C4009CFC64 /* Sources */,
				76CCDDD727EAD1C4009CFC64 /* Frameworks */,
				76CCDDD827EAD1C4009CFC64 /* Resources */,
				7686473F27EEED4500BCC350 /* ShellScript */,
			);
			buildRules = (
			);
			dependencies = (
			);
			name = PaimonMenuBar;
			packageProductDependencies = (
				76085E6027FC23B000960915 /* LaunchAtLogin */,
				76085E6327FC23EA00960915 /* Sparkle */,
				9F38F971281D4D000004D240 /* Defaults */,
				320C466028254F8700C6932E /* Kingfisher */,
			);
			productName = PaimonMenuBar;
			productReference = 76F9AE6B27F570640051CDC8 /* PaimonMenuBar.app */;
			productType = "com.apple.product-type.application";
		};
/* End PBXNativeTarget section */

/* Begin PBXProject section */
		76CCDDD227EAD1C4009CFC64 /* Project object */ = {
			isa = PBXProject;
			attributes = {
				BuildIndependentTargetsInParallel = 1;
				LastSwiftUpdateCheck = 1330;
				LastUpgradeCheck = 1410;
				TargetAttributes = {
					76CCDDD927EAD1C4009CFC64 = {
						CreatedOnToolsVersion = 13.3;
					};
				};
			};
			buildConfigurationList = 76CCDDD527EAD1C4009CFC64 /* Build configuration list for PBXProject "PaimonMenuBar" */;
			compatibilityVersion = "Xcode 13.0";
			developmentRegion = en;
			hasScannedForEncodings = 0;
			knownRegions = (
				en,
				Base,
				"zh-Hans",
			);
			mainGroup = 76CCDDD127EAD1C4009CFC64;
			packageReferences = (
				76085E5F27FC23B000960915 /* XCRemoteSwiftPackageReference "LaunchAtLogin" */,
				76085E6227FC23EA00960915 /* XCRemoteSwiftPackageReference "Sparkle" */,
				9F38F970281D4D000004D240 /* XCRemoteSwiftPackageReference "Defaults" */,
				320C465F28254F8700C6932E /* XCRemoteSwiftPackageReference "Kingfisher" */,
			);
			productRefGroup = 76CCDDD127EAD1C4009CFC64;
			projectDirPath = "";
			projectRoot = "";
			targets = (
				76CCDDD927EAD1C4009CFC64 /* PaimonMenuBar */,
			);
		};
/* End PBXProject section */

/* Begin PBXResourcesBuildPhase section */
		76CCDDD827EAD1C4009CFC64 /* Resources */ = {
			isa = PBXResourcesBuildPhase;
			buildActionMask = 2147483647;
			files = (
				76CCDDE527EAD1C5009CFC64 /* Preview Assets.xcassets in Resources */,
				7621675327F2FC080023F8B2 /* Localizable.strings in Resources */,
				76CCDDE227EAD1C5009CFC64 /* Assets.xcassets in Resources */,
			);
			runOnlyForDeploymentPostprocessing = 0;
		};
/* End PBXResourcesBuildPhase section */

/* Begin PBXShellScriptBuildPhase section */
		7686473F27EEED4500BCC350 /* ShellScript */ = {
			isa = PBXShellScriptBuildPhase;
			alwaysOutOfDate = 1;
			buildActionMask = 2147483647;
			files = (
			);
			inputFileListPaths = (
			);
			inputPaths = (
			);
			outputFileListPaths = (
			);
			outputPaths = (
			);
			runOnlyForDeploymentPostprocessing = 0;
			shellPath = /bin/sh;
			shellScript = "\"${BUILT_PRODUCTS_DIR}/LaunchAtLogin_LaunchAtLogin.bundle/Contents/Resources/copy-helper-swiftpm.sh\"\n";
		};
/* End PBXShellScriptBuildPhase section */

/* Begin PBXSourcesBuildPhase section */
		76CCDDD627EAD1C4009CFC64 /* Sources */ = {
			isa = PBXSourcesBuildPhase;
			buildActionMask = 2147483647;
			files = (
				76E986B627EDD5FC004ECC6C /* MenuExtrasView.swift in Sources */,
				7686474127EF082400BCC350 /* Bundle.swift in Sources */,
				76E429A927EDDE000032313C /* GameRecordUpdater.swift in Sources */,
				9F38F974281D4D120004D240 /* Defaults+Workaround.swift in Sources */,
				323850E1282540EE0097B5C2 /* Compatibility.swift in Sources */,
				76D73BBF27EC650500CCDEA6 /* DataModels.swift in Sources */,
				76C290F027EAFFB000A30C9F /* AppDelegate.swift in Sources */,
				76D73BC127EC67D300CCDEA6 /* Networking.swift in Sources */,
				76F9AE6D27F570D90051CDC8 /* UpdaterViewModel.swift in Sources */,
				9F38F96F281D1BE90004D240 /* Defaults.swift in Sources */,
				76CCDDE027EAD1C4009CFC64 /* SettingsView.swift in Sources */,
				76CCDDDE27EAD1C4009CFC64 /* PaimonMenuBarApp.swift in Sources */,
			);
			runOnlyForDeploymentPostprocessing = 0;
		};
/* End PBXSourcesBuildPhase section */

/* Begin PBXVariantGroup section */
		7621675527F2FC080023F8B2 /* Localizable.strings */ = {
			isa = PBXVariantGroup;
			children = (
				7621675427F2FC080023F8B2 /* en */,
				7621675627F2FC0B0023F8B2 /* zh-Hans */,
			);
			name = Localizable.strings;
			sourceTree = "<group>";
		};
/* End PBXVariantGroup section */

/* Begin XCBuildConfiguration section */
		76CCDDE727EAD1C5009CFC64 /* Debug */ = {
			isa = XCBuildConfiguration;
			buildSettings = {
				ALWAYS_SEARCH_USER_PATHS = NO;
				CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
				CLANG_ANALYZER_NONNULL = YES;
				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
				CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
				CLANG_ENABLE_MODULES = YES;
				CLANG_ENABLE_OBJC_ARC = YES;
				CLANG_ENABLE_OBJC_WEAK = YES;
				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
				CLANG_WARN_BOOL_CONVERSION = YES;
				CLANG_WARN_COMMA = YES;
				CLANG_WARN_CONSTANT_CONVERSION = YES;
				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
				CLANG_WARN_EMPTY_BODY = YES;
				CLANG_WARN_ENUM_CONVERSION = YES;
				CLANG_WARN_INFINITE_RECURSION = YES;
				CLANG_WARN_INT_CONVERSION = YES;
				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
				CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
				CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
				CLANG_WARN_STRICT_PROTOTYPES = YES;
				CLANG_WARN_SUSPICIOUS_MOVE = YES;
				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
				CLANG_WARN_UNREACHABLE_CODE = YES;
				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
				COPY_PHASE_STRIP = NO;
				DEAD_CODE_STRIPPING = YES;
				DEBUG_INFORMATION_FORMAT = dwarf;
				ENABLE_STRICT_OBJC_MSGSEND = YES;
				ENABLE_TESTABILITY = YES;
				GCC_C_LANGUAGE_STANDARD = gnu11;
				GCC_DYNAMIC_NO_PIC = NO;
				GCC_NO_COMMON_BLOCKS = YES;
				GCC_OPTIMIZATION_LEVEL = 0;
				GCC_PREPROCESSOR_DEFINITIONS = (
					"DEBUG=1",
					"$(inherited)",
				);
				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
				GCC_WARN_UNDECLARED_SELECTOR = YES;
				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
				GCC_WARN_UNUSED_FUNCTION = YES;
				GCC_WARN_UNUSED_VARIABLE = YES;
				MACOSX_DEPLOYMENT_TARGET = 12.2;
				MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
				MTL_FAST_MATH = YES;
				ONLY_ACTIVE_ARCH = YES;
				SDKROOT = macosx;
				SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
				SWIFT_OPTIMIZATION_LEVEL = "-Onone";
			};
			name = Debug;
		};
		76CCDDE827EAD1C5009CFC64 /* Release */ = {
			isa = XCBuildConfiguration;
			buildSettings = {
				ALWAYS_SEARCH_USER_PATHS = NO;
				CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
				CLANG_ANALYZER_NONNULL = YES;
				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
				CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
				CLANG_ENABLE_MODULES = YES;
				CLANG_ENABLE_OBJC_ARC = YES;
				CLANG_ENABLE_OBJC_WEAK = YES;
				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
				CLANG_WARN_BOOL_CONVERSION = YES;
				CLANG_WARN_COMMA = YES;
				CLANG_WARN_CONSTANT_CONVERSION = YES;
				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
				CLANG_WARN_EMPTY_BODY = YES;
				CLANG_WARN_ENUM_CONVERSION = YES;
				CLANG_WARN_INFINITE_RECURSION = YES;
				CLANG_WARN_INT_CONVERSION = YES;
				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
				CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
				CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
				CLANG_WARN_STRICT_PROTOTYPES = YES;
				CLANG_WARN_SUSPICIOUS_MOVE = YES;
				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
				CLANG_WARN_UNREACHABLE_CODE = YES;
				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
				COPY_PHASE_STRIP = NO;
				DEAD_CODE_STRIPPING = YES;
				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
				ENABLE_NS_ASSERTIONS = NO;
				ENABLE_STRICT_OBJC_MSGSEND = YES;
				GCC_C_LANGUAGE_STANDARD = gnu11;
				GCC_NO_COMMON_BLOCKS = YES;
				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
				GCC_WARN_UNDECLARED_SELECTOR = YES;
				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
				GCC_WARN_UNUSED_FUNCTION = YES;
				GCC_WARN_UNUSED_VARIABLE = YES;
				MACOSX_DEPLOYMENT_TARGET = 12.2;
				MTL_ENABLE_DEBUG_INFO = NO;
				MTL_FAST_MATH = YES;
				SDKROOT = macosx;
				SWIFT_COMPILATION_MODE = wholemodule;
				SWIFT_OPTIMIZATION_LEVEL = "-O";
			};
			name = Release;
		};
		76CCDDEA27EAD1C5009CFC64 /* Debug */ = {
			isa = XCBuildConfiguration;
			buildSettings = {
				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
				ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
				CODE_SIGN_ENTITLEMENTS = PaimonMenuBar/PaimonMenuBar.entitlements;
				CODE_SIGN_IDENTITY = "Apple Development";
				CODE_SIGN_STYLE = Automatic;
				COMBINE_HIDPI_IMAGES = YES;
				CURRENT_PROJECT_VERSION = 128;
				DEAD_CODE_STRIPPING = YES;
				DEVELOPMENT_ASSET_PATHS = "\"PaimonMenuBar/Preview Content\"";
				DEVELOPMENT_TEAM = W2HGAU9MPP;
				ENABLE_HARDENED_RUNTIME = YES;
				ENABLE_PREVIEWS = YES;
				GENERATE_INFOPLIST_FILE = YES;
				INFOPLIST_FILE = PaimonMenuBar/Info.plist;
				INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.entertainment";
				INFOPLIST_KEY_NSHumanReadableCopyright = "";
				LD_RUNPATH_SEARCH_PATHS = (
					"$(inherited)",
					"@executable_path/../Frameworks",
				);
				MACOSX_DEPLOYMENT_TARGET = 11.0;
				MARKETING_VERSION = 1.12;
				PRODUCT_BUNDLE_IDENTIFIER = spencerwoo.PaimonMenuBar;
				PRODUCT_NAME = "$(TARGET_NAME)";
				SWIFT_EMIT_LOC_STRINGS = YES;
				SWIFT_VERSION = 5.0;
			};
			name = Debug;
		};
		76CCDDEB27EAD1C5009CFC64 /* Release */ = {
			isa = XCBuildConfiguration;
			buildSettings = {
				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
				ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
				CODE_SIGN_ENTITLEMENTS = PaimonMenuBar/PaimonMenuBar.entitlements;
				CODE_SIGN_IDENTITY = "Apple Development";
				CODE_SIGN_STYLE = Automatic;
				COMBINE_HIDPI_IMAGES = YES;
				CURRENT_PROJECT_VERSION = 128;
				DEAD_CODE_STRIPPING = YES;
				DEVELOPMENT_ASSET_PATHS = "\"PaimonMenuBar/Preview Content\"";
				DEVELOPMENT_TEAM = W2HGAU9MPP;
				ENABLE_HARDENED_RUNTIME = YES;
				ENABLE_PREVIEWS = YES;
				GENERATE_INFOPLIST_FILE = YES;
				INFOPLIST_FILE = PaimonMenuBar/Info.plist;
				INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.entertainment";
				INFOPLIST_KEY_NSHumanReadableCopyright = "";
				LD_RUNPATH_SEARCH_PATHS = (
					"$(inherited)",
					"@executable_path/../Frameworks",
				);
				MACOSX_DEPLOYMENT_TARGET = 11.0;
				MARKETING_VERSION = 1.12;
				PRODUCT_BUNDLE_IDENTIFIER = spencerwoo.PaimonMenuBar;
				PRODUCT_NAME = "$(TARGET_NAME)";
				SWIFT_EMIT_LOC_STRINGS = YES;
				SWIFT_VERSION = 5.0;
			};
			name = Release;
		};
/* End XCBuildConfiguration section */

/* Begin XCConfigurationList section */
		76CCDDD527EAD1C4009CFC64 /* Build configuration list for PBXProject "PaimonMenuBar" */ = {
			isa = XCConfigurationList;
			buildConfigurations = (
				76CCDDE727EAD1C5009CFC64 /* Debug */,
				76CCDDE827EAD1C5009CFC64 /* Release */,
			);
			defaultConfigurationIsVisible = 0;
			defaultConfigurationName = Release;
		};
		76CCDDE927EAD1C5009CFC64 /* Build configuration list for PBXNativeTarget "PaimonMenuBar" */ = {
			isa = XCConfigurationList;
			buildConfigurations = (
				76CCDDEA27EAD1C5009CFC64 /* Debug */,
				76CCDDEB27EAD1C5009CFC64 /* Release */,
			);
			defaultConfigurationIsVisible = 0;
			defaultConfigurationName = Release;
		};
/* End XCConfigurationList section */

/* Begin XCRemoteSwiftPackageReference section */
		320C465F28254F8700C6932E /* XCRemoteSwiftPackageReference "Kingfisher" */ = {
			isa = XCRemoteSwiftPackageReference;
			repositoryURL = "https://github.com/onevcat/Kingfisher.git";
			requirement = {
				kind = upToNextMajorVersion;
				minimumVersion = 7.0.0;
			};
		};
		76085E5F27FC23B000960915 /* XCRemoteSwiftPackageReference "LaunchAtLogin" */ = {
			isa = XCRemoteSwiftPackageReference;
			repositoryURL = "https://github.com/sindresorhus/LaunchAtLogin";
			requirement = {
				branch = main;
				kind = branch;
			};
		};
		76085E6227FC23EA00960915 /* XCRemoteSwiftPackageReference "Sparkle" */ = {
			isa = XCRemoteSwiftPackageReference;
			repositoryURL = "https://github.com/sparkle-project/Sparkle";
			requirement = {
				branch = 2.x;
				kind = branch;
			};
		};
		9F38F970281D4D000004D240 /* XCRemoteSwiftPackageReference "Defaults" */ = {
			isa = XCRemoteSwiftPackageReference;
			repositoryURL = "https://github.com/sindresorhus/Defaults";
			requirement = {
				branch = main;
				kind = branch;
			};
		};
/* End XCRemoteSwiftPackageReference section */

/* Begin XCSwiftPackageProductDependency section */
		320C466028254F8700C6932E /* Kingfisher */ = {
			isa = XCSwiftPackageProductDependency;
			package = 320C465F28254F8700C6932E /* XCRemoteSwiftPackageReference "Kingfisher" */;
			productName = Kingfisher;
		};
		76085E6027FC23B000960915 /* LaunchAtLogin */ = {
			isa = XCSwiftPackageProductDependency;
			package = 76085E5F27FC23B000960915 /* XCRemoteSwiftPackageReference "LaunchAtLogin" */;
			productName = LaunchAtLogin;
		};
		76085E6327FC23EA00960915 /* Sparkle */ = {
			isa = XCSwiftPackageProductDependency;
			package = 76085E6227FC23EA00960915 /* XCRemoteSwiftPackageReference "Sparkle" */;
			productName = Sparkle;
		};
		9F38F971281D4D000004D240 /* Defaults */ = {
			isa = XCSwiftPackageProductDependency;
			package = 9F38F970281D4D000004D240 /* XCRemoteSwiftPackageReference "Defaults" */;
			productName = Defaults;
		};
/* End XCSwiftPackageProductDependency section */
	};
	rootObject = 76CCDDD227EAD1C4009CFC64 /* Project object */;
}


================================================
FILE: PaimonMenuBar.xcodeproj/project.xcworkspace/contents.xcworkspacedata
================================================
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
   version = "1.0">
   <FileRef
      location = "self:">
   </FileRef>
</Workspace>


================================================
FILE: PaimonMenuBar.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>IDEDidComputeMac32BitWarning</key>
	<true/>
</dict>
</plist>


================================================
FILE: PaimonMenuBar.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>PreviewsEnabled</key>
	<false/>
</dict>
</plist>


================================================
FILE: PaimonMenuBar.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
================================================
{
  "pins" : [
    {
      "identity" : "defaults",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/sindresorhus/Defaults",
      "state" : {
        "branch" : "main",
        "revision" : "3535f3d088113cf24705014eec6a17f0fd73237f"
      }
    },
    {
      "identity" : "kingfisher",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/onevcat/Kingfisher.git",
      "state" : {
        "revision" : "59eb199fdb8dd47733624e8b0277822d0232579e",
        "version" : "7.2.2"
      }
    },
    {
      "identity" : "launchatlogin",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/sindresorhus/LaunchAtLogin",
      "state" : {
        "branch" : "main",
        "revision" : "e8171b3e38a2816f579f58f3dac1522aa39efe41"
      }
    },
    {
      "identity" : "sparkle",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/sparkle-project/Sparkle",
      "state" : {
        "branch" : "2.x",
        "revision" : "f250bead4b943ef9711c61274a1f52e380afa0e8"
      }
    }
  ],
  "version" : 2
}


================================================
FILE: PaimonMenuBar.xcodeproj/xcshareddata/xcschemes/PaimonMenuBar.xcscheme
================================================
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
   LastUpgradeVersion = "1410"
   version = "1.3">
   <BuildAction
      parallelizeBuildables = "YES"
      buildImplicitDependencies = "YES">
      <BuildActionEntries>
         <BuildActionEntry
            buildForTesting = "YES"
            buildForRunning = "YES"
            buildForProfiling = "YES"
            buildForArchiving = "YES"
            buildForAnalyzing = "YES">
            <BuildableReference
               BuildableIdentifier = "primary"
               BlueprintIdentifier = "76CCDDD927EAD1C4009CFC64"
               BuildableName = "PaimonMenuBar.app"
               BlueprintName = "PaimonMenuBar"
               ReferencedContainer = "container:PaimonMenuBar.xcodeproj">
            </BuildableReference>
         </BuildActionEntry>
      </BuildActionEntries>
   </BuildAction>
   <TestAction
      buildConfiguration = "Debug"
      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
      shouldUseLaunchSchemeArgsEnv = "YES">
      <Testables>
      </Testables>
   </TestAction>
   <LaunchAction
      buildConfiguration = "Debug"
      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
      launchStyle = "0"
      useCustomWorkingDirectory = "NO"
      ignoresPersistentStateOnLaunch = "NO"
      debugDocumentVersioning = "YES"
      debugServiceExtension = "internal"
      allowLocationSimulation = "YES">
      <BuildableProductRunnable
         runnableDebuggingMode = "0">
         <BuildableReference
            BuildableIdentifier = "primary"
            BlueprintIdentifier = "76CCDDD927EAD1C4009CFC64"
            BuildableName = "PaimonMenuBar.app"
            BlueprintName = "PaimonMenuBar"
            ReferencedContainer = "container:PaimonMenuBar.xcodeproj">
         </BuildableReference>
      </BuildableProductRunnable>
   </LaunchAction>
   <ProfileAction
      buildConfiguration = "Release"
      shouldUseLaunchSchemeArgsEnv = "YES"
      savedToolIdentifier = ""
      useCustomWorkingDirectory = "NO"
      debugDocumentVersioning = "YES">
      <BuildableProductRunnable
         runnableDebuggingMode = "0">
         <BuildableReference
            BuildableIdentifier = "primary"
            BlueprintIdentifier = "76CCDDD927EAD1C4009CFC64"
            BuildableName = "PaimonMenuBar.app"
            BlueprintName = "PaimonMenuBar"
            ReferencedContainer = "container:PaimonMenuBar.xcodeproj">
         </BuildableReference>
      </BuildableProductRunnable>
   </ProfileAction>
   <AnalyzeAction
      buildConfiguration = "Debug">
   </AnalyzeAction>
   <ArchiveAction
      buildConfiguration = "Release"
      revealArchiveInOrganizer = "YES">
   </ArchiveAction>
</Scheme>


================================================
FILE: README.md
================================================
<div align="center">
  <img src="Assets/logo.png" alt="logo" width="160" height="160" />
  <h3><code>PaimonMenuBar</code></h3>
  <p><em>Track real-time Genshin Impact stats in your macOS menubar</em></p>

  <img src="https://img.shields.io/badge/uses-SwiftUI-f05138?labelColor=282c34&logo=swift" alt="Use Swift" />
  <img src="https://img.shields.io/badge/macOS-11.0+-f05138?labelColor=282c34&logo=apple" alt="macOS 11.0+" />
  <a href="https://github.com/spencerwooo/PaimonMenuBar/releases/latest"><img src="https://img.shields.io/github/v/release/spencerwooo/PaimonMenuBar?labelColor=282c34&logo=GitHub" alt="GitHub Release" /></a>
</div>

## What's this?

![screenshot](Assets/screenshot.jpg)

> Paimon helps you track your Genshin Impact daily resin, expeditions, and more — straight in your macOS menu bar.

Paimon can help you —

* 🌙 Keep track of your daily resin.
* 💰 Monitor your daily expeditions and real-time realm currency.
* 🏁 Remind you about your daily commissions and weekly boss fights.
* 🍯 And notify you when your parametric transformer is ready to use.

Basically, `PaimonMenuBar` lives in your macOS menu bar quietly, and offers you a nice way of monitoring your in-game real-time stats when you need to check them.

> **Note**
>
> `PaimonMenuBar` is made with SwiftUI, designed for and native to macOS.

## Download

[![GitHub Release](https://img.shields.io/github/v/release/spencerwooo/PaimonMenuBar?labelColor=282c34&logo=GitHub&style=for-the-badge)](https://github.com/spencerwooo/PaimonMenuBar/releases/latest)

## Things to know

1. Paimon uses the official Hoyoverse API found in either [米游社 (for CN players)](https://bbs.mihoyo.com/ys/) or [HoYoLAB (for Global players)](https://www.hoyolab.com/home).
2. Yes, Paimon needs your cookie. It is so that Paimon can request said API on your behalf, and fetch those in-game stats periodically. Rest assured that **the cookie is only stored locally.**
3. Check [FAQ](https://paimon.swo.moe/) if you have anymore questions.

## Credits

* Credits to [@Chawong](https://www.pixiv.net/en/artworks/92415888) for the logo. (Love from Hu Tao :heart:)
* iOS widget (Scriptable): [[闲聊杂谈][工具分享] iOS 快捷指令/小组件](https://bbs.nga.cn/read.php?tid=29801567)
* Friendly browser extension alternative: [daidr/paimon-webext](https://github.com/daidr/paimon-webext)
* Friendly Windows alternative: [ArvinZJC/PaimonTray](https://github.com/ArvinZJC/PaimonTray)

<details>
<summary>Development notes.</summary>

## TO-DO

* [x] Menu bar of varying height.
* [x] Configurable data refresh rate.
* [x] Start at login.
* [x] `i18n` support for at least Simplified Chinese and English.
* [x] Manual refresh button.
* [x] Code-sign and publish as `.dmg`.
* [x] Auto-updates and check for update.
* [x] Custom website and help for acquiring the cookie.
* [x] Help button beside the text field for entering the cookie.
* [x] Support for cn and global genshin accounts (米游社 and hoyolab).
* [x] Backward-compatibility for macOS 11.0.
* [x] Better first-time installation experience (guidance for initial setup).
* ~~[ ] Support for multiple accounts?~~

## Releasing a new version

* Create a build in Xcode, bump the build number, and notarize build.
* Create a new release on GitHub with a new version tag and increment the build number.
* Use `create-dmg` to create the `.dmg` file:

  ```bash
  create-dmg PaimonMenuBar.app
  ```

* Update appcast.xml with the new version tag and build number:

  ```bash
  cd <PATH_TO_SPARKLE>/artifacts/sparkle/bin
  ./generate_appcast <PATH_TO_PROJECT>/PaimonMenuBar/Build/
  ```

* Profit.

</details>

## License

[MIT](LICENSE)

<div align="center">
  <img src="Assets/footer.png" />
  <em>made with ❤️ by <a href="https://spencerwoo.com">spencer woo</a></em>
</div>


================================================
FILE: appcast.xml
================================================
<?xml version="1.0" standalone="yes"?>
<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" version="2.0">
    <channel>
        <title>PaimonMenuBar</title>
        <item>
            <title>1.12</title>
            <pubDate>Thu, 08 Dec 2022 13:12:40 +0800</pubDate>
            <sparkle:version>128</sparkle:version>
            <sparkle:shortVersionString>1.12</sparkle:shortVersionString>
            <sparkle:minimumSystemVersion>11.0</sparkle:minimumSystemVersion>
            <enclosure url="https://github.com/spencerwooo/PaimonMenuBar/releases/download/v1.12.128/PaimonMenuBar.1.12.dmg" length="5637918" type="application/octet-stream" sparkle:edSignature="PrBMHAQ7DkzuCa/jODOndBxVExumkQaHu2YKxmbRBBu2KSGK1thFzJfFmAfJjRn6G/QeT4y+fm5yLI0w7mKOCQ=="/>
        </item>
        <item>
            <title>1.12</title>
            <pubDate>Thu, 08 Dec 2022 01:42:15 +0800</pubDate>
            <sparkle:version>126</sparkle:version>
            <sparkle:shortVersionString>1.12</sparkle:shortVersionString>
            <sparkle:minimumSystemVersion>11.0</sparkle:minimumSystemVersion>
            <enclosure url="https://github.com/spencerwooo/PaimonMenuBar/releases/download/v1.12.126/PaimonMenuBar.1.12.dmg" length="5635852" type="application/octet-stream" sparkle:edSignature="WGyHmjHDNg0tYRq4G1kFhm40ObGPiw+7ms5Ddnbme/PHiSwS3NKegtZ7/0sjM0j3uw2BqTSQR9COLb6FesySDg=="/>
        </item>
        <item>
            <title>1.11</title>
            <pubDate>Sun, 09 Oct 2022 15:29:45 +0800</pubDate>
            <sparkle:version>124</sparkle:version>
            <sparkle:shortVersionString>1.11</sparkle:shortVersionString>
            <sparkle:minimumSystemVersion>11.0</sparkle:minimumSystemVersion>
            <enclosure url="https://github.com/spencerwooo/PaimonMenuBar/releases/download/v1.11.124/PaimonMenuBar.1.11.dmg" length="5632634" type="application/octet-stream" sparkle:edSignature="fhprCrniicFz7VHnEG/cULokwhn6cXBbUscXcU03BnhkXgg6zTr8wxYtoDt+Ku4gYAV+2P3c6cesjmwhMUujBw=="/>
        </item>
        <item>
            <title>1.10</title>
            <pubDate>Thu, 11 Aug 2022 16:42:19 +0800</pubDate>
            <sparkle:version>123</sparkle:version>
            <sparkle:shortVersionString>1.10</sparkle:shortVersionString>
            <sparkle:minimumSystemVersion>11.0</sparkle:minimumSystemVersion>
            <enclosure url="https://github.com/spencerwooo/PaimonMenuBar/releases/download/v1.10.123/PaimonMenuBar.1.10.dmg" length="5626931" type="application/octet-stream" sparkle:edSignature="ZRHF1dGDOf8eurp9O1Kpm2TiVyY+836+dYflkTQ87Gbq/fgjwRxz/g79nabjx6KlGI20q+Ve+Cybxc2+EUI6CQ=="/>
        </item>
        <item>
            <title>1.9</title>
            <pubDate>Thu, 09 Jun 2022 20:19:39 +0800</pubDate>
            <sparkle:version>120</sparkle:version>
            <sparkle:shortVersionString>1.9</sparkle:shortVersionString>
            <sparkle:minimumSystemVersion>11.0</sparkle:minimumSystemVersion>
            <enclosure url="https://github.com/spencerwooo/PaimonMenuBar/releases/download/v1.9.120/PaimonMenuBar.1.9.dmg" length="4581848" type="application/octet-stream" sparkle:edSignature="KnEcO3MHDP9T+cxt8ru6M2BMtGox1RgbpqYKiW1I3vLEp1DqFNf6PqgefCgq7mGCwAIhC+9CvXhRqtwZcsgrDA=="/>
        </item>
        <item>
            <title>1.8</title>
            <pubDate>Tue, 07 Jun 2022 21:24:06 +0800</pubDate>
            <sparkle:version>114</sparkle:version>
            <sparkle:shortVersionString>1.8</sparkle:shortVersionString>
            <sparkle:minimumSystemVersion>11.0</sparkle:minimumSystemVersion>
            <enclosure url="https://github.com/spencerwooo/PaimonMenuBar/releases/download/v1.8.114/PaimonMenuBar.1.8.dmg" length="4496866" type="application/octet-stream" sparkle:edSignature="qPcQ3fub9Ffoh7wvscFoPMmgZ8i/AmQ7LcqLYJFOAzvajpZpFbofKmFUFVTA6jzC83yBL5MNYdliCjhIjkmjBw=="/>
        </item>
        <item>
            <title>1.7</title>
            <pubDate>Fri, 13 May 2022 23:20:25 +0800</pubDate>
            <sparkle:version>113</sparkle:version>
            <sparkle:shortVersionString>1.7</sparkle:shortVersionString>
            <sparkle:minimumSystemVersion>11.0</sparkle:minimumSystemVersion>
            <enclosure url="https://github.com/spencerwooo/PaimonMenuBar/releases/download/v1.7.113/PaimonMenuBar.1.7.dmg" length="4492440" type="application/octet-stream" sparkle:edSignature="TpXQhQYI85GCPlbJyqqboWUaBpAd9HXus/c06QvxxT1crZlpd0Wb4sscpN3Z4xSktGqBmJIsGNL58cXCe3BOBQ=="/>
        </item>
        <item>
            <title>1.7</title>
            <pubDate>Mon, 09 May 2022 13:49:30 +0800</pubDate>
            <sparkle:version>111</sparkle:version>
            <sparkle:shortVersionString>1.7</sparkle:shortVersionString>
            <sparkle:minimumSystemVersion>11.0</sparkle:minimumSystemVersion>
            <enclosure url="https://github.com/spencerwooo/PaimonMenuBar/releases/download/v1.7.111/PaimonMenuBar.1.7.dmg" length="4488485" type="application/octet-stream" sparkle:edSignature="31aSlKt4D99sMHpIJg9Mz/aXLF3kpZqJPFyjJWZjK7/8agwH43WEc5vNiMd67uJwgJNRYNQXacfAj9F7oZaMAA=="/>
        </item>
        <item>
            <title>1.6</title>
            <pubDate>Sun, 01 May 2022 20:37:03 +0800</pubDate>
            <sparkle:version>110</sparkle:version>
            <sparkle:shortVersionString>1.6</sparkle:shortVersionString>
            <sparkle:minimumSystemVersion>12.0</sparkle:minimumSystemVersion>
            <enclosure url="https://github.com/spencerwooo/PaimonMenuBar/releases/download/v1.6.110/PaimonMenuBar.1.6.dmg" length="3517801" type="application/octet-stream" sparkle:edSignature="beNk/XPJhjCTGbQjyMvlMTKeq4g1gosbmJw93k67m5rZ49k956WFeVnpCuXm7kBEd6yQLijV3AiTTiAn51xoDg=="/>
        </item>
        <item>
            <title>1.5</title>
            <pubDate>Sat, 30 Apr 2022 15:07:47 +0800</pubDate>
            <sparkle:version>108</sparkle:version>
            <sparkle:shortVersionString>1.5</sparkle:shortVersionString>
            <sparkle:minimumSystemVersion>12.0</sparkle:minimumSystemVersion>
            <enclosure url="https://github.com/spencerwooo/PaimonMenuBar/releases/download/v1.5.108/PaimonMenuBar.1.5.dmg" length="3377362" type="application/octet-stream" sparkle:edSignature="tgup1mfveJhZaOzd0wB9uhSpdVv+ejRrWB2oybprGABRMU39qkm6fi/UGISfyhsVtw098IQo4syYNwIXWGyuDA=="/>
        </item>
        <item>
            <title>1.4</title>
            <pubDate>Thu, 28 Apr 2022 22:35:09 +0800</pubDate>
            <sparkle:version>106</sparkle:version>
            <sparkle:shortVersionString>1.4</sparkle:shortVersionString>
            <sparkle:minimumSystemVersion>12.0</sparkle:minimumSystemVersion>
            <enclosure url="https://github.com/spencerwooo/PaimonMenuBar/releases/download/v1.4.106/PaimonMenuBar.1.4.dmg" length="3391221" type="application/octet-stream" sparkle:edSignature="rsh/lK4OdFEJbZSJUSgALXOAuhYj7aB0LKGjF+nFFn4aMmjmtEcB7GmIVMMQ0nYaKYkjQ0nVnzMj9bD0fpNLBg=="/>
        </item>
        <item>
            <title>1.4</title>
            <pubDate>Tue, 26 Apr 2022 13:56:31 +0800</pubDate>
            <sparkle:version>104</sparkle:version>
            <sparkle:shortVersionString>1.4</sparkle:shortVersionString>
            <sparkle:minimumSystemVersion>12.0</sparkle:minimumSystemVersion>
            <enclosure url="https://github.com/spencerwooo/PaimonMenuBar/releases/download/v1.4.104/PaimonMenuBar.1.4.dmg" length="3390386" type="application/octet-stream" sparkle:edSignature="aUxb5JAhHTKpf8AOePkq2qA86vq7gWRHdz/uqzDX2WwyesoeOLOHKyK2Uxt1TvvaVTNLNI24VSXk5UKEObTyDA=="/>
        </item>
        <item>
            <title>1.3</title>
            <pubDate>Tue, 26 Apr 2022 01:31:47 +0800</pubDate>
            <sparkle:version>103</sparkle:version>
            <sparkle:shortVersionString>1.3</sparkle:shortVersionString>
            <sparkle:minimumSystemVersion>12.0</sparkle:minimumSystemVersion>
            <enclosure url="https://github.com/spencerwooo/PaimonMenuBar/releases/download/v1.3.103/PaimonMenuBar.1.3.dmg" length="3387934" type="application/octet-stream" sparkle:edSignature="FUECvyK5S+M1mOhGX4NbNRpxXpfuHQt/dCamaKX2D4bNZ7bCn7CMz33VDv3C9W341Al6z7RLoexoxh7nmkvYAA=="/>
        </item>
        <item>
            <title>1.2</title>
            <pubDate>Thu, 31 Mar 2022 16:20:33 +0800</pubDate>
            <sparkle:version>102</sparkle:version>
            <sparkle:shortVersionString>1.2</sparkle:shortVersionString>
            <sparkle:minimumSystemVersion>12.0</sparkle:minimumSystemVersion>
            <enclosure url="https://github.com/spencerwooo/PaimonMenuBar/releases/download/v1.2.102/PaimonMenuBar.1.2.dmg" length="3385535" type="application/octet-stream" sparkle:edSignature="O4RCB0x5RhaBps9bFRO1Goiltn2+JCrV4x7Yi8t0bruSUBAk1sY4Mt2sKh0WksHy4ouHtdy4niDadUTK8NnBAQ=="/>
        </item>
        <item>
            <title>1.2</title>
            <pubDate>Thu, 31 Mar 2022 14:31:26 +0800</pubDate>
            <sparkle:version>101</sparkle:version>
            <sparkle:shortVersionString>1.2</sparkle:shortVersionString>
            <sparkle:minimumSystemVersion>12.0</sparkle:minimumSystemVersion>
            <enclosure url="https://github.com/spencerwooo/PaimonMenuBar/releases/download/v1.2.101/PaimonMenuBar.1.2.dmg" length="3385341" type="application/octet-stream" sparkle:edSignature="QFYmtv5jrU8z1/8+tTuQ3lLBlvDlIAtgCbIKCqySNdGmhnn9keH3DZH44myHiZYW7QJ6WZaqQVmOCdvJrEygAQ=="/>
        </item>
        <item>
            <title>1.2</title>
            <pubDate>Thu, 31 Mar 2022 14:17:48 +0800</pubDate>
            <sparkle:version>100</sparkle:version>
            <sparkle:shortVersionString>1.2</sparkle:shortVersionString>
            <sparkle:minimumSystemVersion>12.0</sparkle:minimumSystemVersion>
            <enclosure url="https://github.com/spencerwooo/PaimonMenuBar/releases/download/v1.2.100/PaimonMenuBar.1.2.dmg" length="3385317" type="application/octet-stream" sparkle:edSignature="nf8UE3ezWsBedF7EHxsvqihF3GJSsxx9lmukc0+LpNGo58Y6oMygMAFfc+yC4rU4BtG9qS7lpm9kk9Apwzs0Bg=="/>
        </item>
    </channel>
</rss>
Download .txt
gitextract_kmzlkky9/

├── .gitignore
├── Docs/
│   ├── .eslintrc.json
│   ├── .gitignore
│   ├── README.md
│   ├── components/
│   │   ├── AvailableNow.tsx
│   │   ├── DownloadButton.tsx
│   │   ├── Faq.tsx
│   │   ├── Features.tsx
│   │   ├── Footer.tsx
│   │   ├── GitHubButton.tsx
│   │   ├── Head.tsx
│   │   ├── Hero.tsx
│   │   ├── HowToGetMyCookie.tsx
│   │   ├── PaimonCan.tsx
│   │   ├── PaimonCookie.tsx
│   │   ├── PaimonUses.tsx
│   │   ├── ReleaseInfo.tsx
│   │   └── Screenshot3D.tsx
│   ├── next-env.d.ts
│   ├── next.config.js
│   ├── package.json
│   ├── pages/
│   │   ├── _app.tsx
│   │   ├── api/
│   │   │   └── hello.ts
│   │   ├── index.tsx
│   │   └── types.d.ts
│   ├── postcss.config.js
│   ├── public/
│   │   └── site.webmanifest
│   ├── styles/
│   │   └── globals.css
│   ├── tailwind.config.js
│   └── tsconfig.json
├── LICENSE
├── PaimonMenuBar/
│   ├── AppDelegate.swift
│   ├── Assets.xcassets/
│   │   ├── AccentColor.colorset/
│   │   │   └── Contents.json
│   │   ├── AppIcon.appiconset/
│   │   │   └── Contents.json
│   │   ├── Commision.imageset/
│   │   │   └── Contents.json
│   │   ├── Contents.json
│   │   ├── Domain.imageset/
│   │   │   └── Contents.json
│   │   ├── Expedition.imageset/
│   │   │   └── Contents.json
│   │   ├── FragileResin.imageset/
│   │   │   └── Contents.json
│   │   ├── JarOfRiches.imageset/
│   │   │   └── Contents.json
│   │   ├── KokomiSad.imageset/
│   │   │   └── Contents.json
│   │   └── ParametricTransformer.imageset/
│   │       └── Contents.json
│   ├── Bundle.swift
│   ├── Compatibility.swift
│   ├── DataModels.swift
│   ├── Defaults+Workaround.swift
│   ├── Defaults.swift
│   ├── GameRecordUpdater.swift
│   ├── Info.plist
│   ├── MenuExtrasView.swift
│   ├── Networking.swift
│   ├── PaimonMenuBar.entitlements
│   ├── PaimonMenuBarApp.swift
│   ├── Preview Content/
│   │   └── Preview Assets.xcassets/
│   │       └── Contents.json
│   ├── SettingsView.swift
│   ├── UpdaterViewModel.swift
│   ├── en.lproj/
│   │   └── Localizable.strings
│   └── zh-Hans.lproj/
│       └── Localizable.strings
├── PaimonMenuBar.xcodeproj/
│   ├── project.pbxproj
│   ├── project.xcworkspace/
│   │   ├── contents.xcworkspacedata
│   │   └── xcshareddata/
│   │       ├── IDEWorkspaceChecks.plist
│   │       ├── WorkspaceSettings.xcsettings
│   │       └── swiftpm/
│   │           └── Package.resolved
│   └── xcshareddata/
│       └── xcschemes/
│           └── PaimonMenuBar.xcscheme
├── README.md
└── appcast.xml
Download .txt
SYMBOL INDEX (5 symbols across 4 files)

FILE: Docs/components/ReleaseInfo.tsx
  type reactionKeys (line 13) | type reactionKeys = keyof typeof reactionToEmoji

FILE: Docs/pages/_app.tsx
  function MyApp (line 9) | function MyApp({ Component, pageProps }: AppProps) {

FILE: Docs/pages/api/hello.ts
  type Data (line 4) | type Data = {
  function handler (line 8) | function handler(

FILE: Docs/pages/types.d.ts
  type AppReleaseData (line 1) | type AppReleaseData = {
Condensed preview — 66 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (139K chars).
[
  {
    "path": ".gitignore",
    "chars": 2171,
    "preview": "# Xcode\n#\n# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore\n\n"
  },
  {
    "path": "Docs/.eslintrc.json",
    "chars": 40,
    "preview": "{\n  \"extends\": \"next/core-web-vitals\"\n}\n"
  },
  {
    "path": "Docs/.gitignore",
    "chars": 371,
    "preview": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pn"
  },
  {
    "path": "Docs/README.md",
    "chars": 1582,
    "preview": "This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js"
  },
  {
    "path": "Docs/components/AvailableNow.tsx",
    "chars": 1086,
    "preview": "import type { AppReleaseData } from '../pages/types'\nimport Image from \"next/image\"\n\nimport DownloadButton from './Downl"
  },
  {
    "path": "Docs/components/DownloadButton.tsx",
    "chars": 660,
    "preview": "import { RiDownloadCloud2Line } from 'react-icons/ri'\n\nconst DownloadButton = ({\n  tagName,\n  downloadUrl,\n  tailwindSty"
  },
  {
    "path": "Docs/components/Faq.tsx",
    "chars": 1933,
    "preview": "const Faq = () => (\n  <article className=\"prose prose-invert max-w-none\">\n    <h2>FAQs</h2>\n\n    <ol>\n      <li>\n       "
  },
  {
    "path": "Docs/components/Features.tsx",
    "chars": 1723,
    "preview": "import Image, { StaticImageData } from \"next/image\"\n\nimport daily from '../images/daily.png'\nimport expedition from '../"
  },
  {
    "path": "Docs/components/Footer.tsx",
    "chars": 632,
    "preview": "import { RiHeartPulseLine, RiHeartsLine } from 'react-icons/ri'\n\nconst Footer = () => (\n  <div className=\"relative px-4 "
  },
  {
    "path": "Docs/components/GitHubButton.tsx",
    "chars": 517,
    "preview": "import { RiGithubLine } from 'react-icons/ri'\n\nconst GitHubButton = () => {\n  return (\n    <a\n      className=\"px-4 py-2"
  },
  {
    "path": "Docs/components/Head.tsx",
    "chars": 501,
    "preview": "import Head from 'next/head'\n\nconst Meta = () => (\n  <Head>\n    <title>PaimonMenuBar</title>\n    <meta name=\"description"
  },
  {
    "path": "Docs/components/Hero.tsx",
    "chars": 2236,
    "preview": "import type { AppReleaseData } from '../pages/types'\n\nimport Image from \"next/image\"\n\nimport Screenshot3D from '../compo"
  },
  {
    "path": "Docs/components/HowToGetMyCookie.tsx",
    "chars": 2289,
    "preview": "import Image from \"next/image\"\n\nimport hutaoSleepy from '../images/hutao-sleepy.png'\nimport cookieScreenshot from '../im"
  },
  {
    "path": "Docs/components/PaimonCan.tsx",
    "chars": 979,
    "preview": "import Image from \"next/image\"\nimport paimonMighty from '../images/paimon-mighty.png'\n\nconst PaimonCan = () => (\n  <div>"
  },
  {
    "path": "Docs/components/PaimonCookie.tsx",
    "chars": 900,
    "preview": "import Image from \"next/image\"\nimport luminePlease from '../images/lumine-please.png'\n\nconst PaimonCookie = () => (\n  <d"
  },
  {
    "path": "Docs/components/PaimonUses.tsx",
    "chars": 899,
    "preview": "import Image from \"next/image\"\nimport zhongliThink from '../images/zhongli-think.png'\n\nconst PaimonUses = () => (\n  <div"
  },
  {
    "path": "Docs/components/ReleaseInfo.tsx",
    "chars": 1993,
    "preview": "import type { AppReleaseData } from '../pages/types'\n\nconst reactionToEmoji = {\n  '+1': '👍',\n  '-1': '👎',\n  laugh: '😂',\n"
  },
  {
    "path": "Docs/components/Screenshot3D.tsx",
    "chars": 472,
    "preview": "import Image from \"next/image\"\nimport Atropos from 'atropos/react'\nimport screenshot from '../images/screenshot-transpar"
  },
  {
    "path": "Docs/next-env.d.ts",
    "chars": 201,
    "preview": "/// <reference types=\"next\" />\n/// <reference types=\"next/image-types/global\" />\n\n// NOTE: This file should not be edite"
  },
  {
    "path": "Docs/next.config.js",
    "chars": 119,
    "preview": "/** @type {import('next').NextConfig} */\nconst nextConfig = {\n  reactStrictMode: true,\n\n}\n\nmodule.exports = nextConfig\n"
  },
  {
    "path": "Docs/package.json",
    "chars": 708,
    "preview": "{\n  \"name\": \"docs\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"next dev\",\n    \"build\": \"next bu"
  },
  {
    "path": "Docs/pages/_app.tsx",
    "chars": 283,
    "preview": "import '../styles/globals.css'\nimport 'atropos/css'\n\nimport '@fontsource/mulish/400.css'\nimport '@fontsource/mulish/700."
  },
  {
    "path": "Docs/pages/api/hello.ts",
    "chars": 314,
    "preview": "// Next.js API route support: https://nextjs.org/docs/api-routes/introduction\nimport type { NextApiRequest, NextApiRespo"
  },
  {
    "path": "Docs/pages/index.tsx",
    "chars": 1998,
    "preview": "import type { GetStaticProps } from 'next'\nimport type { AppReleaseData } from './types'\nimport Image from \"next/image\"\n"
  },
  {
    "path": "Docs/pages/types.d.ts",
    "chars": 399,
    "preview": "export type AppReleaseData = {\n  html_url: string\n  tag_name: string\n  name: string\n  published_at: string\n  assets: Arr"
  },
  {
    "path": "Docs/postcss.config.js",
    "chars": 82,
    "preview": "module.exports = {\n  plugins: {\n    tailwindcss: {},\n    autoprefixer: {},\n  },\n}\n"
  },
  {
    "path": "Docs/public/site.webmanifest",
    "chars": 263,
    "preview": "{\"name\":\"\",\"short_name\":\"\",\"icons\":[{\"src\":\"/android-chrome-192x192.png\",\"sizes\":\"192x192\",\"type\":\"image/png\"},{\"src\":\"/"
  },
  {
    "path": "Docs/styles/globals.css",
    "chars": 98,
    "preview": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\nbody {\n  background-color: #2c3740;\n}\n"
  },
  {
    "path": "Docs/tailwind.config.js",
    "chars": 351,
    "preview": "const defaultTheme = require('tailwindcss/defaultTheme')\n\nmodule.exports = {\n  content: [\n    \"./pages/**/*.{js,ts,jsx,t"
  },
  {
    "path": "Docs/tsconfig.json",
    "chars": 509,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"es5\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    \"sk"
  },
  {
    "path": "LICENSE",
    "chars": 1068,
    "preview": "MIT License\n\nCopyright (c) 2022 Spencer Woo\n\nPermission is hereby granted, free of charge, to any person obtaining a cop"
  },
  {
    "path": "PaimonMenuBar/AppDelegate.swift",
    "chars": 4226,
    "preview": "//\n//  AppDelegate.swift\n//  PaimonMenuBar\n//\n//  Created by Spencer Woo on 2022/3/23.\n//\n\nimport AppKit\nimport Defaults"
  },
  {
    "path": "PaimonMenuBar/Assets.xcassets/AccentColor.colorset/Contents.json",
    "chars": 123,
    "preview": "{\n  \"colors\" : [\n    {\n      \"idiom\" : \"universal\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }"
  },
  {
    "path": "PaimonMenuBar/Assets.xcassets/AppIcon.appiconset/Contents.json",
    "chars": 1265,
    "preview": "{\n  \"images\" : [\n    {\n      \"filename\" : \"hutao16@1.png\",\n      \"idiom\" : \"mac\",\n      \"scale\" : \"1x\",\n      \"size\" : \""
  },
  {
    "path": "PaimonMenuBar/Assets.xcassets/Commision.imageset/Contents.json",
    "chars": 307,
    "preview": "{\n  \"images\" : [\n    {\n      \"filename\" : \"commision.svg\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"1x\"\n    },\n    "
  },
  {
    "path": "PaimonMenuBar/Assets.xcassets/Contents.json",
    "chars": 63,
    "preview": "{\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "PaimonMenuBar/Assets.xcassets/Domain.imageset/Contents.json",
    "chars": 376,
    "preview": "{\n  \"images\" : [\n    {\n      \"filename\" : \"Domain.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"1x\"\n    },\n    {\n "
  },
  {
    "path": "PaimonMenuBar/Assets.xcassets/Expedition.imageset/Contents.json",
    "chars": 388,
    "preview": "{\n  \"images\" : [\n    {\n      \"filename\" : \"Expedition.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"1x\"\n    },\n   "
  },
  {
    "path": "PaimonMenuBar/Assets.xcassets/FragileResin.imageset/Contents.json",
    "chars": 397,
    "preview": "{\n  \"images\" : [\n    {\n      \"filename\" : \"fragile_resin@1.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"1x\"\n    }"
  },
  {
    "path": "PaimonMenuBar/Assets.xcassets/JarOfRiches.imageset/Contents.json",
    "chars": 391,
    "preview": "{\n  \"images\" : [\n    {\n      \"filename\" : \"JarOfRiches.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"1x\"\n    },\n  "
  },
  {
    "path": "PaimonMenuBar/Assets.xcassets/KokomiSad.imageset/Contents.json",
    "chars": 386,
    "preview": "{\n  \"images\" : [\n    {\n      \"filename\" : \"kokomi_sad.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"1x\"\n    },\n   "
  },
  {
    "path": "PaimonMenuBar/Assets.xcassets/ParametricTransformer.imageset/Contents.json",
    "chars": 421,
    "preview": "{\n  \"images\" : [\n    {\n      \"filename\" : \"ParametricTransformer@1.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"1"
  },
  {
    "path": "PaimonMenuBar/Bundle.swift",
    "chars": 464,
    "preview": "//\n//  Bundle.swift\n//  PaimonMenuBar\n//\n//  Created by Spencer Woo on 2022/3/26.\n//\n\nimport Foundation\n\nextension Bundl"
  },
  {
    "path": "PaimonMenuBar/Compatibility.swift",
    "chars": 2882,
    "preview": "//\n//  Compatibility.swift\n//  PaimonMenuBar\n//\n//  Created by DreamPiggy on 2022/5/6.\n//\n\nimport Foundation\nimport Swif"
  },
  {
    "path": "PaimonMenuBar/DataModels.swift",
    "chars": 3413,
    "preview": "//\n//  DataModels.swift\n//  PaimonMenuBar\n//\n//  Created by Spencer Woo on 2022/3/24.\n//\n\nimport Defaults\nimport Foundat"
  },
  {
    "path": "PaimonMenuBar/Defaults+Workaround.swift",
    "chars": 1783,
    "preview": "// See https://github.com/sindresorhus/Defaults/blob/main/workaround.md\n\nimport Defaults\nimport Foundation\n\npublic exten"
  },
  {
    "path": "PaimonMenuBar/Defaults.swift",
    "chars": 1402,
    "preview": "//\n//  Defaults.swift\n//  PaimonMenuBar\n//\n//  Created by Breezewish on 2022/4/30.\n//\n\nimport Defaults\nimport Foundation"
  },
  {
    "path": "PaimonMenuBar/GameRecordUpdater.swift",
    "chars": 9235,
    "preview": "//\n//  GameRecordUpdater.swift\n//  PaimonMenuBar\n//\n//  Created by Spencer Woo on 2022/3/25.\n//\n\nimport Combine\nimport D"
  },
  {
    "path": "PaimonMenuBar/Info.plist",
    "chars": 448,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "PaimonMenuBar/MenuExtrasView.swift",
    "chars": 11832,
    "preview": "//\n//  MenuView.swift\n//  PaimonMenuBar\n//\n//  Created by Spencer Woo on 2022/3/25.\n//\n\nimport Defaults\nimport Foundatio"
  },
  {
    "path": "PaimonMenuBar/Networking.swift",
    "chars": 2741,
    "preview": "//\n//  Networking.swift\n//  PaimonMenuBar\n//\n//  Created by Spencer Woo on 2022/3/24.\n//\n\nimport CryptoKit\nimport Defaul"
  },
  {
    "path": "PaimonMenuBar/PaimonMenuBar.entitlements",
    "chars": 565,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "PaimonMenuBar/PaimonMenuBarApp.swift",
    "chars": 541,
    "preview": "//\n//  PaimonMenuBarApp.swift\n//  PaimonMenuBar\n//\n//  Created by Spencer Woo on 2022/3/23.\n//\n\nimport SwiftUI\n\n@main\nst"
  },
  {
    "path": "PaimonMenuBar/Preview Content/Preview Assets.xcassets/Contents.json",
    "chars": 63,
    "preview": "{\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "PaimonMenuBar/SettingsView.swift",
    "chars": 11196,
    "preview": "//\n//  ContentView.swift\n//  PaimonMenuBar\n//\n//  Created by Spencer Woo on 2022/3/23.\n//\n\nimport Defaults\nimport Launch"
  },
  {
    "path": "PaimonMenuBar/UpdaterViewModel.swift",
    "chars": 729,
    "preview": "//\n//  UpdaterViewModel.swift\n//  PaimonMenuBar\n//\n//  Created by Spencer Woo on 2022/3/31.\n//\n\nimport Foundation\nimport"
  },
  {
    "path": "PaimonMenuBar/en.lproj/Localizable.strings",
    "chars": 1,
    "preview": "\n"
  },
  {
    "path": "PaimonMenuBar/zh-Hans.lproj/Localizable.strings",
    "chars": 2250,
    "preview": "\"*Updating every 1-3 hours is sufficient, to prevent from being captcha-ed. Don't worry, as Paimon will auto-update the "
  },
  {
    "path": "PaimonMenuBar.xcodeproj/project.pbxproj",
    "chars": 22913,
    "preview": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 55;\n\tobjects = {\n\n/* Begin PBXBuildFile section *"
  },
  {
    "path": "PaimonMenuBar.xcodeproj/project.xcworkspace/contents.xcworkspacedata",
    "chars": 135,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n   version = \"1.0\">\n   <FileRef\n      location = \"self:\">\n   </FileRef"
  },
  {
    "path": "PaimonMenuBar.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
    "chars": 238,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "PaimonMenuBar.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings",
    "chars": 226,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "PaimonMenuBar.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved",
    "chars": 1100,
    "preview": "{\n  \"pins\" : [\n    {\n      \"identity\" : \"defaults\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://gi"
  },
  {
    "path": "PaimonMenuBar.xcodeproj/xcshareddata/xcschemes/PaimonMenuBar.xcscheme",
    "chars": 2894,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n   LastUpgradeVersion = \"1410\"\n   version = \"1.3\">\n   <BuildAction\n      "
  },
  {
    "path": "README.md",
    "chars": 3754,
    "preview": "<div align=\"center\">\n  <img src=\"Assets/logo.png\" alt=\"logo\" width=\"160\" height=\"160\" />\n  <h3><code>PaimonMenuBar</code"
  },
  {
    "path": "appcast.xml",
    "chars": 10008,
    "preview": "<?xml version=\"1.0\" standalone=\"yes\"?>\n<rss xmlns:sparkle=\"http://www.andymatuschak.org/xml-namespaces/sparkle\" version="
  }
]

About this extraction

This page contains the full source code of the spencerwooo/PaimonMenuBar GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 66 files (123.6 KB), approximately 36.6k tokens, and a symbol index with 5 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!