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'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?

> 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
[](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>
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
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.