├── .gitignore
├── GoogleService-Info.plist
├── Old.zip
├── Screenshot
├── detailScreen.png
├── homeScreen.png
├── newHome.PNG
├── newRecent.PNG
├── randomScreen.png
├── ratingDetail.PNG
├── searchScreen.png
└── songListScreen.png
├── chafenqi.xcodeproj
├── project.pbxproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── swiftpm
│ │ └── Package.resolved
├── xcshareddata
│ └── xcschemes
│ │ ├── chafenqi.xcscheme
│ │ ├── chafenqiMini.xcscheme
│ │ ├── chafenqiNotifier.xcscheme
│ │ ├── infoWidgetExtension.xcscheme
│ │ └── updater.xcscheme
└── xcuserdata
│ └── louis.xcuserdatad
│ └── xcschemes
│ └── xcschememanagement.plist
├── chafenqi
├── App
│ ├── AppDelegate.swift
│ ├── CFQError.swift
│ ├── CFQSharedValue.swift
│ └── chafenqiApp.swift
├── Assets.xcassets
│ ├── AppIcon.appiconset
│ │ ├── 100.png
│ │ ├── 1024.png
│ │ ├── 114.png
│ │ ├── 120.png
│ │ ├── 144.png
│ │ ├── 152.png
│ │ ├── 167.png
│ │ ├── 180.png
│ │ ├── 20.png
│ │ ├── 29.png
│ │ ├── 40.png
│ │ ├── 50.png
│ │ ├── 57.png
│ │ ├── 58.png
│ │ ├── 60.png
│ │ ├── 72.png
│ │ ├── 76.png
│ │ ├── 80.png
│ │ ├── 87.png
│ │ └── Contents.json
│ ├── Contents.json
│ ├── Icon.imageset
│ │ ├── Contents.json
│ │ └── 新建画布1.png
│ ├── Perks
│ │ ├── Contents.json
│ │ ├── alt_delta.imageset
│ │ │ ├── Contents.json
│ │ │ └── alt_delta.png
│ │ ├── alt_playerinfo.imageset
│ │ │ ├── Contents.json
│ │ │ └── alt_playerinfo.png
│ │ ├── delta.imageset
│ │ │ ├── Contents.json
│ │ │ └── delta.png
│ │ ├── history.imageset
│ │ │ ├── Contents.json
│ │ │ └── history.png
│ │ ├── playerinfo.imageset
│ │ │ ├── Contents.json
│ │ │ └── playerinfo.png
│ │ └── quicklook.imageset
│ │ │ ├── Contents.json
│ │ │ └── quicklook.png
│ ├── first_prompt.imageset
│ │ ├── Contents.json
│ │ └── first_prompt.png
│ ├── login_prompt.imageset
│ │ ├── Contents.json
│ │ └── login_prompt.png
│ ├── nameplate_penguin.imageset
│ │ ├── Contents.json
│ │ └── nameplate_penguin.png
│ ├── nameplate_salt.imageset
│ │ ├── Contents.json
│ │ └── nameplate_salt.png
│ ├── select_mode_prompt.imageset
│ │ ├── Contents.json
│ │ └── select_mode_prompt.png
│ ├── send_prompt.imageset
│ │ ├── Contents.json
│ │ └── send_prompt.png
│ ├── settings_prompt.imageset
│ │ ├── Contents.json
│ │ └── settings_prompt.png
│ ├── switch_mode_prompt.imageset
│ │ ├── Contents.json
│ │ └── switch_mode_prompt.png
│ ├── switch_prompt.imageset
│ │ ├── Contents.json
│ │ └── switch_prompt.png
│ └── tutorial_prompt.imageset
│ │ ├── Contents.json
│ │ └── tutorial_prompt.png
├── CoreData
│ ├── CacheController.swift
│ ├── ImageCache.xcdatamodeld
│ │ └── ImageCache.xcdatamodel
│ │ │ └── contents
│ ├── WidgetData.xcdatamodeld
│ │ └── WidgetData.xcdatamodel
│ │ │ └── contents
│ └── WidgetDataController.swift
├── Extension
│ ├── FilterExtension.swift
│ ├── FoundationExtension.swift
│ ├── URLResponseExtension.swift
│ └── ViewExtension.swift
├── Helper
│ ├── AsyncImage
│ │ ├── AsyncImage.swift
│ │ ├── ImageCache.swift
│ │ └── ImageLoader.swift
│ ├── ChartIdConverter.swift
│ ├── ChartImageGrabber.swift
│ ├── ChunithmDataGrabber.swift
│ ├── DataTool.swift
│ ├── DateTool.swift
│ ├── Logger.swift
│ ├── MaimaiDataGrabber.swift
│ ├── ModalView.swift
│ ├── ScreenshotMaker.swift
│ ├── ShareSheetMaker.swift
│ └── SongViewModifier.swift
├── Info.plist
├── Intents.intentdefinition
├── Object
│ ├── AlertToastManager.swift
│ ├── AlertToastModel.swift
│ └── RegexManager.swift
├── Preview Content
│ └── Preview Assets.xcassets
│ │ └── Contents.json
├── Resource
│ ├── ChafenqiColor.swift
│ ├── IdMap.json
│ └── dlc.json
├── Service
│ ├── QuickActionService.swift
│ └── TunnelManagerService.swift
├── Struct
│ ├── CFQAuxData.swift
│ ├── CFQData.swift
│ ├── CFQNUser.swift
│ ├── CFQPersistentData.swift
│ ├── CFQRemoteOptions.swift
│ ├── CFQServer.swift
│ ├── CFQTeam.swift
│ ├── CFQTeamServer.swift
│ ├── Chunithm
│ │ ├── ChunithmLeaderboardEntry.swift
│ │ └── ChunithmMusicData.swift
│ ├── Comment
│ │ ├── Comment.swift
│ │ └── UserComment.swift
│ ├── CustomAlert.swift
│ ├── Filter
│ │ └── CFQFilterOptions.swift
│ ├── Maimai
│ │ ├── MaimaiChartStat.swift
│ │ ├── MaimaiGenreData.swift
│ │ ├── MaimaiLeaderboardEntry.swift
│ │ ├── MaimaiSongData.swift
│ │ ├── MaimaiVersionData.swift
│ │ └── chafenqi 2023-02-11 15-48-57
│ │ │ ├── DistributionSummary.plist
│ │ │ ├── ExportOptions.plist
│ │ │ ├── Packaging.log
│ │ │ └── chafenqi.ipa
│ ├── Server
│ │ ├── CFQMusicStat.swift
│ │ ├── CFQUserInfo.swift
│ │ ├── CFQUserUploadStatus.swift
│ │ ├── Chunithm
│ │ │ ├── UserChunithmEntry.swift
│ │ │ └── UserChunithmExtraEntry.swift
│ │ ├── Maimai
│ │ │ ├── UserMaimaiEntry.swift
│ │ │ └── UserMaimaiExtraEntry.swift
│ │ └── Team
│ │ │ ├── TeamActivity.swift
│ │ │ ├── TeamBasicInfo.swift
│ │ │ ├── TeamBulletinBoardEntry.swift
│ │ │ ├── TeamCourseRecord.swift
│ │ │ ├── TeamCreatePayload.swift
│ │ │ ├── TeamInfo.swift
│ │ │ ├── TeamMember.swift
│ │ │ ├── TeamPendingMember.swift
│ │ │ └── TeamUpdateCoursePayload.swift
│ └── Widget
│ │ └── WidgetData.swift
├── View
│ ├── v1
│ │ ├── ScoreView
│ │ │ └── B30View.swift
│ │ ├── SongView
│ │ │ ├── ChunithmBasicView.swift
│ │ │ └── SongBarView.swift
│ │ └── TopView
│ │ │ ├── ChunithmHomeView.swift
│ │ │ ├── ChunithmHomeView_BACKUP_30897.swift
│ │ │ ├── ChunithmListView.swift
│ │ │ ├── HomeView
│ │ │ └── Module
│ │ │ │ └── RatingAnalysisView.swift
│ │ │ ├── LoginView.swift
│ │ │ └── MaimaiHomeView.swift
│ └── v2
│ │ ├── Comment
│ │ ├── CommentCell.swift
│ │ ├── CommentComposer.swift
│ │ └── CommentDetail.swift
│ │ ├── Custom
│ │ ├── CharacterCapsule.swift
│ │ ├── CustomContextPreview.swift
│ │ ├── GradeBadgeView.swift
│ │ ├── LevelBlockView.swift
│ │ └── TextInfoView.swift
│ │ ├── Delta
│ │ ├── Charts
│ │ │ ├── PCDeltaChart.swift
│ │ │ └── RatingDeltaChart.swift
│ │ ├── DeltaDetailView.swift
│ │ ├── DeltaListView.swift
│ │ ├── DeltaPlayList.swift
│ │ └── DeltaShortLook.swift
│ │ ├── Easter
│ │ └── ClickGameView.swift
│ │ ├── Home
│ │ ├── HomeDelta.swift
│ │ ├── HomeLeaderboard.swift
│ │ ├── HomeNameplate.swift
│ │ ├── HomeRating.swift
│ │ ├── HomeRecent.swift
│ │ ├── HomeTeam.swift
│ │ └── HomeView.swift
│ │ ├── Info
│ │ ├── ChunithmList
│ │ │ ├── InfoChunithmCharacterList.swift
│ │ │ ├── InfoChunithmMapIconList.swift
│ │ │ ├── InfoChunithmNameplateList.swift
│ │ │ ├── InfoChunithmSkillList.swift
│ │ │ ├── InfoChunithmTicketList.swift
│ │ │ └── InfoChunithmTrophyList.swift
│ │ ├── MaimaiList
│ │ │ ├── InfoMaimaiCharacterList.swift
│ │ │ ├── InfoMaimaiClearList.swift
│ │ │ ├── InfoMaimaiFrameList.swift
│ │ │ ├── InfoMaimaiNameplateList.swift
│ │ │ └── InfoMaimaiTrophyList.swift
│ │ ├── PlayerChunithmInfoView.swift
│ │ └── PlayerMaimaiInfoView.swift
│ │ ├── Leaderboard
│ │ ├── LeaderboardTabView.swift
│ │ └── LeaderboardView.swift
│ │ ├── LoginView.swift
│ │ ├── Premium
│ │ └── NotPremiumView.swift
│ │ ├── Rating
│ │ ├── RatingBannerView.swift
│ │ ├── RatingListView.swift
│ │ ├── RatingShareView.swift
│ │ └── RatingShortLook.swift
│ │ ├── Recent
│ │ ├── RecentDetail.swift
│ │ └── RecentListView.swift
│ │ ├── RootView.swift
│ │ ├── Settings
│ │ ├── LogView.swift
│ │ ├── PerksPreview
│ │ │ ├── PerkSheetView.swift
│ │ │ └── PerksString.swift
│ │ ├── RedeemView.swift
│ │ ├── Settings.swift
│ │ ├── SettingsHomeArrangement.swift
│ │ ├── SponsorView.swift
│ │ ├── TokenUploderView.swift
│ │ ├── User
│ │ │ └── UserLinkOptionView.swift
│ │ └── Widget
│ │ │ ├── SettingsWidgetConfig.swift
│ │ │ ├── WidgetCustomSelectionViews.swift
│ │ │ ├── WidgetOptionPickers.swift
│ │ │ └── WidgetPreviews.swift
│ │ ├── Song
│ │ ├── Charts
│ │ │ └── SongScoreTrendChart.swift
│ │ ├── List
│ │ │ ├── Filter
│ │ │ │ ├── MultiplePicker.swift
│ │ │ │ ├── MultiplePickerItem.swift
│ │ │ │ └── SongListFilterView.swift
│ │ │ └── SongListView.swift
│ │ ├── SongChartView.swift
│ │ ├── SongCoverView.swift
│ │ ├── SongDetailView.swift
│ │ ├── SongEntryListView.swift
│ │ ├── SongItemView.swift
│ │ ├── SongStatsDetailView.swift
│ │ └── Stats
│ │ │ ├── SongLeaderboardView.swift
│ │ │ ├── SongStatsTabBarView.swift
│ │ │ └── SongStatsView.swift
│ │ ├── Team
│ │ ├── Info
│ │ │ ├── TeamActivityView.swift
│ │ │ ├── TeamBulletinView.swift
│ │ │ ├── TeamCourseView.swift
│ │ │ ├── TeamInfoPage.swift
│ │ │ └── TeamMemberView.swift
│ │ ├── Setting
│ │ │ ├── TeamCourseSetting.swift
│ │ │ ├── TeamMemberSetting.swift
│ │ │ ├── TeamPendingMemberSetting.swift
│ │ │ └── TeamSettingsPage.swift
│ │ ├── TeamIntroductionPage.swift
│ │ ├── TeamLandingPage.swift
│ │ └── TeamLeaderboardPage.swift
│ │ ├── Tool
│ │ └── RandomSongView.swift
│ │ └── Updater
│ │ ├── UpdaterHelpView.swift
│ │ ├── UpdaterQRCodeView.swift
│ │ ├── UpdaterRootView.swift
│ │ ├── UpdaterView.swift
│ │ └── UpdaterWelcomeView.swift
└── chafenqi.entitlements
├── chafenqiMini
├── BuildURLHandler.swift
├── Info.plist
├── IntentHandler.swift
├── Intents.intentdefinition
└── chafenqiMini.entitlements
├── chafenqiNotifier
├── Info.plist
├── NotificationService.swift
└── chafenqiNotifier.entitlements
├── chafenqiTests
└── chafenqiTests.swift
├── chafenqiUITests
├── chafenqiUITests.swift
└── chafenqiUITestsLaunchTests.swift
├── infoWidget
├── Assets.xcassets
│ ├── AccentColor.colorset
│ │ └── Contents.json
│ ├── AppIcon.appiconset
│ │ └── Contents.json
│ ├── Contents.json
│ ├── WidgetBackground.colorset
│ │ └── Contents.json
│ ├── penguin.imageset
│ │ ├── Contents.json
│ │ └── nameplate_penguin.png
│ └── salt.imageset
│ │ ├── Contents.json
│ │ └── nameplate_salt.png
├── Info.plist
├── InfoWidgetEntryView.swift
├── UserInfoFetcher.swift
├── infoWidget.intentdefinition
├── infoWidget.swift
└── infoWidgetBundle.swift
├── infoWidgetExtension.entitlements
├── readme.md
└── updater
├── ChafenqiTunnelProvider.swift
├── Info.plist
├── LocalServer
├── ChunithmNetHandler.swift
├── ConnectHandler.swift
├── GlueHandler.swift
└── ProxyServer.swift
├── Server
└── ProxyServer.swift
├── UpdaterTunnelProvider.swift
└── updater.entitlements
/.gitignore:
--------------------------------------------------------------------------------
1 | xcuserdata/
2 | *.xcuserstate
3 | DerivedData/
4 | .DS_Store
5 | */.DS_Store
6 |
7 | chafenqi.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
8 | /chafenqi.xcodeproj/xcuserdata/louis.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist
9 | /.idea
10 | .vscode/settings.json
11 |
--------------------------------------------------------------------------------
/GoogleService-Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | API_KEY
6 | AIzaSyCm-cnaPGWhIySJ5uOFqzE8IwOMoOXIzp0
7 | GCM_SENDER_ID
8 | 111132677417
9 | PLIST_VERSION
10 | 1
11 | BUNDLE_ID
12 | com.nltv.chafenqi
13 | PROJECT_ID
14 | chafenqi-ios
15 | STORAGE_BUCKET
16 | chafenqi-ios.appspot.com
17 | IS_ADS_ENABLED
18 |
19 | IS_ANALYTICS_ENABLED
20 |
21 | IS_APPINVITE_ENABLED
22 |
23 | IS_GCM_ENABLED
24 |
25 | IS_SIGNIN_ENABLED
26 |
27 | GOOGLE_APP_ID
28 | 1:111132677417:ios:c93215a6296f2543d15338
29 |
30 |
31 |
--------------------------------------------------------------------------------
/Old.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Louiswu2011/chafenqi/206cb81997741633392f743c6c06d7ec754e7e13/Old.zip
--------------------------------------------------------------------------------
/Screenshot/detailScreen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Louiswu2011/chafenqi/206cb81997741633392f743c6c06d7ec754e7e13/Screenshot/detailScreen.png
--------------------------------------------------------------------------------
/Screenshot/homeScreen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Louiswu2011/chafenqi/206cb81997741633392f743c6c06d7ec754e7e13/Screenshot/homeScreen.png
--------------------------------------------------------------------------------
/Screenshot/newHome.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Louiswu2011/chafenqi/206cb81997741633392f743c6c06d7ec754e7e13/Screenshot/newHome.PNG
--------------------------------------------------------------------------------
/Screenshot/newRecent.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Louiswu2011/chafenqi/206cb81997741633392f743c6c06d7ec754e7e13/Screenshot/newRecent.PNG
--------------------------------------------------------------------------------
/Screenshot/randomScreen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Louiswu2011/chafenqi/206cb81997741633392f743c6c06d7ec754e7e13/Screenshot/randomScreen.png
--------------------------------------------------------------------------------
/Screenshot/ratingDetail.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Louiswu2011/chafenqi/206cb81997741633392f743c6c06d7ec754e7e13/Screenshot/ratingDetail.PNG
--------------------------------------------------------------------------------
/Screenshot/searchScreen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Louiswu2011/chafenqi/206cb81997741633392f743c6c06d7ec754e7e13/Screenshot/searchScreen.png
--------------------------------------------------------------------------------
/Screenshot/songListScreen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Louiswu2011/chafenqi/206cb81997741633392f743c6c06d7ec754e7e13/Screenshot/songListScreen.png
--------------------------------------------------------------------------------
/chafenqi.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/chafenqi/App/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2023/4/18.
6 | //
7 |
8 | import UIKit
9 | import OneSignal
10 | import FirebaseCore
11 | import FirebaseCrashlytics
12 | import FirebaseAnalytics
13 | import FirebasePerformance
14 |
15 | class AppDelegate: NSObject, UIApplicationDelegate {
16 | private let actionService = QuickActionService.shared
17 |
18 | func application(
19 | _ application: UIApplication,
20 | configurationForConnecting connectingSceneSession: UISceneSession,
21 | options: UIScene.ConnectionOptions
22 | ) -> UISceneConfiguration {
23 | if let shortcutItem = options.shortcutItem {
24 | actionService.action = QuickAction(item: shortcutItem)
25 | }
26 |
27 | let configuration = UISceneConfiguration(
28 | name: connectingSceneSession.configuration.name,
29 | sessionRole: connectingSceneSession.role
30 | )
31 | configuration.delegateClass = SceneDelegate.self
32 | return configuration
33 | }
34 |
35 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
36 | // Remove this method to stop OneSignal Debugging
37 | // OneSignal.setLogLevel(.LL_VERBOSE, visualLevel: .LL_NONE)
38 |
39 | OneSignal.initWithLaunchOptions(launchOptions)
40 | OneSignal.setAppId("61d8cb1c-6de2-4b50-af87-f419b2d24ece")
41 |
42 | OneSignal.promptForPushNotifications(userResponse: { accepted in
43 | print("User accepted notification: \(accepted)")
44 | })
45 |
46 | FirebaseApp.configure()
47 |
48 | // Set your customer userId
49 | // OneSignal.setExternalUserId("userId")
50 |
51 | #if DEBUG
52 | Bundle(path: "/Applications/InjectionIII.app/Contents/Resources/iOSInjection.bundle")?.load()
53 | #endif
54 |
55 | return true
56 | }
57 | }
58 |
59 | class SceneDelegate: NSObject, UIWindowSceneDelegate {
60 | private let actionService = QuickActionService.shared
61 |
62 | func windowScene(
63 | _ windowScene: UIWindowScene,
64 | performActionFor shortcutItem: UIApplicationShortcutItem,
65 | completionHandler: @escaping (Bool) -> Void
66 | ) {
67 | actionService.action = QuickAction(item: shortcutItem)
68 | completionHandler(true)
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/chafenqi/App/CFQError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CFQError.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2023/1/18.
6 | //
7 |
8 | import Foundation
9 |
10 | enum CFQError: Error {
11 | case AuthenticationFailedError
12 | case IOError(file: String)
13 | case unsupportedError(reason: String)
14 | case emptyResponseError
15 | case requestTimeoutError
16 | case invalidResponseError(response: String)
17 | case LoadingError
18 | case BadRequestError
19 | }
20 |
--------------------------------------------------------------------------------
/chafenqi/App/CFQSharedValue.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CFQSharedValue.swift
3 | // chafenqi
4 | //
5 | // Created by Louis Wu on 2025/01/26.
6 | //
7 |
8 | import Foundation
9 |
10 | struct SharedValues {
11 | static let serverAddress = "43.139.107.206"
12 | static let apiServerAddress = "http://\(serverAddress):8998/"
13 | static let uploadServerAddress = "http://\(serverAddress):9030/"
14 | static let proxyServerPort = 8999
15 | }
16 |
--------------------------------------------------------------------------------
/chafenqi/Assets.xcassets/AppIcon.appiconset/100.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Louiswu2011/chafenqi/206cb81997741633392f743c6c06d7ec754e7e13/chafenqi/Assets.xcassets/AppIcon.appiconset/100.png
--------------------------------------------------------------------------------
/chafenqi/Assets.xcassets/AppIcon.appiconset/1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Louiswu2011/chafenqi/206cb81997741633392f743c6c06d7ec754e7e13/chafenqi/Assets.xcassets/AppIcon.appiconset/1024.png
--------------------------------------------------------------------------------
/chafenqi/Assets.xcassets/AppIcon.appiconset/114.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Louiswu2011/chafenqi/206cb81997741633392f743c6c06d7ec754e7e13/chafenqi/Assets.xcassets/AppIcon.appiconset/114.png
--------------------------------------------------------------------------------
/chafenqi/Assets.xcassets/AppIcon.appiconset/120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Louiswu2011/chafenqi/206cb81997741633392f743c6c06d7ec754e7e13/chafenqi/Assets.xcassets/AppIcon.appiconset/120.png
--------------------------------------------------------------------------------
/chafenqi/Assets.xcassets/AppIcon.appiconset/144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Louiswu2011/chafenqi/206cb81997741633392f743c6c06d7ec754e7e13/chafenqi/Assets.xcassets/AppIcon.appiconset/144.png
--------------------------------------------------------------------------------
/chafenqi/Assets.xcassets/AppIcon.appiconset/152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Louiswu2011/chafenqi/206cb81997741633392f743c6c06d7ec754e7e13/chafenqi/Assets.xcassets/AppIcon.appiconset/152.png
--------------------------------------------------------------------------------
/chafenqi/Assets.xcassets/AppIcon.appiconset/167.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Louiswu2011/chafenqi/206cb81997741633392f743c6c06d7ec754e7e13/chafenqi/Assets.xcassets/AppIcon.appiconset/167.png
--------------------------------------------------------------------------------
/chafenqi/Assets.xcassets/AppIcon.appiconset/180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Louiswu2011/chafenqi/206cb81997741633392f743c6c06d7ec754e7e13/chafenqi/Assets.xcassets/AppIcon.appiconset/180.png
--------------------------------------------------------------------------------
/chafenqi/Assets.xcassets/AppIcon.appiconset/20.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Louiswu2011/chafenqi/206cb81997741633392f743c6c06d7ec754e7e13/chafenqi/Assets.xcassets/AppIcon.appiconset/20.png
--------------------------------------------------------------------------------
/chafenqi/Assets.xcassets/AppIcon.appiconset/29.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Louiswu2011/chafenqi/206cb81997741633392f743c6c06d7ec754e7e13/chafenqi/Assets.xcassets/AppIcon.appiconset/29.png
--------------------------------------------------------------------------------
/chafenqi/Assets.xcassets/AppIcon.appiconset/40.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Louiswu2011/chafenqi/206cb81997741633392f743c6c06d7ec754e7e13/chafenqi/Assets.xcassets/AppIcon.appiconset/40.png
--------------------------------------------------------------------------------
/chafenqi/Assets.xcassets/AppIcon.appiconset/50.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Louiswu2011/chafenqi/206cb81997741633392f743c6c06d7ec754e7e13/chafenqi/Assets.xcassets/AppIcon.appiconset/50.png
--------------------------------------------------------------------------------
/chafenqi/Assets.xcassets/AppIcon.appiconset/57.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Louiswu2011/chafenqi/206cb81997741633392f743c6c06d7ec754e7e13/chafenqi/Assets.xcassets/AppIcon.appiconset/57.png
--------------------------------------------------------------------------------
/chafenqi/Assets.xcassets/AppIcon.appiconset/58.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Louiswu2011/chafenqi/206cb81997741633392f743c6c06d7ec754e7e13/chafenqi/Assets.xcassets/AppIcon.appiconset/58.png
--------------------------------------------------------------------------------
/chafenqi/Assets.xcassets/AppIcon.appiconset/60.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Louiswu2011/chafenqi/206cb81997741633392f743c6c06d7ec754e7e13/chafenqi/Assets.xcassets/AppIcon.appiconset/60.png
--------------------------------------------------------------------------------
/chafenqi/Assets.xcassets/AppIcon.appiconset/72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Louiswu2011/chafenqi/206cb81997741633392f743c6c06d7ec754e7e13/chafenqi/Assets.xcassets/AppIcon.appiconset/72.png
--------------------------------------------------------------------------------
/chafenqi/Assets.xcassets/AppIcon.appiconset/76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Louiswu2011/chafenqi/206cb81997741633392f743c6c06d7ec754e7e13/chafenqi/Assets.xcassets/AppIcon.appiconset/76.png
--------------------------------------------------------------------------------
/chafenqi/Assets.xcassets/AppIcon.appiconset/80.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Louiswu2011/chafenqi/206cb81997741633392f743c6c06d7ec754e7e13/chafenqi/Assets.xcassets/AppIcon.appiconset/80.png
--------------------------------------------------------------------------------
/chafenqi/Assets.xcassets/AppIcon.appiconset/87.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Louiswu2011/chafenqi/206cb81997741633392f743c6c06d7ec754e7e13/chafenqi/Assets.xcassets/AppIcon.appiconset/87.png
--------------------------------------------------------------------------------
/chafenqi/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/chafenqi/Assets.xcassets/Icon.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "新建画布1.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/chafenqi/Assets.xcassets/Icon.imageset/新建画布1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Louiswu2011/chafenqi/206cb81997741633392f743c6c06d7ec754e7e13/chafenqi/Assets.xcassets/Icon.imageset/新建画布1.png
--------------------------------------------------------------------------------
/chafenqi/Assets.xcassets/Perks/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/chafenqi/Assets.xcassets/Perks/alt_delta.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "alt_delta.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/chafenqi/Assets.xcassets/Perks/alt_delta.imageset/alt_delta.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Louiswu2011/chafenqi/206cb81997741633392f743c6c06d7ec754e7e13/chafenqi/Assets.xcassets/Perks/alt_delta.imageset/alt_delta.png
--------------------------------------------------------------------------------
/chafenqi/Assets.xcassets/Perks/alt_playerinfo.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "alt_playerinfo.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/chafenqi/Assets.xcassets/Perks/alt_playerinfo.imageset/alt_playerinfo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Louiswu2011/chafenqi/206cb81997741633392f743c6c06d7ec754e7e13/chafenqi/Assets.xcassets/Perks/alt_playerinfo.imageset/alt_playerinfo.png
--------------------------------------------------------------------------------
/chafenqi/Assets.xcassets/Perks/delta.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "delta.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/chafenqi/Assets.xcassets/Perks/delta.imageset/delta.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Louiswu2011/chafenqi/206cb81997741633392f743c6c06d7ec754e7e13/chafenqi/Assets.xcassets/Perks/delta.imageset/delta.png
--------------------------------------------------------------------------------
/chafenqi/Assets.xcassets/Perks/history.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "history.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/chafenqi/Assets.xcassets/Perks/history.imageset/history.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Louiswu2011/chafenqi/206cb81997741633392f743c6c06d7ec754e7e13/chafenqi/Assets.xcassets/Perks/history.imageset/history.png
--------------------------------------------------------------------------------
/chafenqi/Assets.xcassets/Perks/playerinfo.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "playerinfo.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/chafenqi/Assets.xcassets/Perks/playerinfo.imageset/playerinfo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Louiswu2011/chafenqi/206cb81997741633392f743c6c06d7ec754e7e13/chafenqi/Assets.xcassets/Perks/playerinfo.imageset/playerinfo.png
--------------------------------------------------------------------------------
/chafenqi/Assets.xcassets/Perks/quicklook.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "quicklook.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/chafenqi/Assets.xcassets/Perks/quicklook.imageset/quicklook.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Louiswu2011/chafenqi/206cb81997741633392f743c6c06d7ec754e7e13/chafenqi/Assets.xcassets/Perks/quicklook.imageset/quicklook.png
--------------------------------------------------------------------------------
/chafenqi/Assets.xcassets/first_prompt.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "first_prompt.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/chafenqi/Assets.xcassets/first_prompt.imageset/first_prompt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Louiswu2011/chafenqi/206cb81997741633392f743c6c06d7ec754e7e13/chafenqi/Assets.xcassets/first_prompt.imageset/first_prompt.png
--------------------------------------------------------------------------------
/chafenqi/Assets.xcassets/login_prompt.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "login_prompt.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/chafenqi/Assets.xcassets/login_prompt.imageset/login_prompt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Louiswu2011/chafenqi/206cb81997741633392f743c6c06d7ec754e7e13/chafenqi/Assets.xcassets/login_prompt.imageset/login_prompt.png
--------------------------------------------------------------------------------
/chafenqi/Assets.xcassets/nameplate_penguin.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "nameplate_penguin.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/chafenqi/Assets.xcassets/nameplate_penguin.imageset/nameplate_penguin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Louiswu2011/chafenqi/206cb81997741633392f743c6c06d7ec754e7e13/chafenqi/Assets.xcassets/nameplate_penguin.imageset/nameplate_penguin.png
--------------------------------------------------------------------------------
/chafenqi/Assets.xcassets/nameplate_salt.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "nameplate_salt.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/chafenqi/Assets.xcassets/nameplate_salt.imageset/nameplate_salt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Louiswu2011/chafenqi/206cb81997741633392f743c6c06d7ec754e7e13/chafenqi/Assets.xcassets/nameplate_salt.imageset/nameplate_salt.png
--------------------------------------------------------------------------------
/chafenqi/Assets.xcassets/select_mode_prompt.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "select_mode_prompt.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/chafenqi/Assets.xcassets/select_mode_prompt.imageset/select_mode_prompt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Louiswu2011/chafenqi/206cb81997741633392f743c6c06d7ec754e7e13/chafenqi/Assets.xcassets/select_mode_prompt.imageset/select_mode_prompt.png
--------------------------------------------------------------------------------
/chafenqi/Assets.xcassets/send_prompt.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "send_prompt.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/chafenqi/Assets.xcassets/send_prompt.imageset/send_prompt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Louiswu2011/chafenqi/206cb81997741633392f743c6c06d7ec754e7e13/chafenqi/Assets.xcassets/send_prompt.imageset/send_prompt.png
--------------------------------------------------------------------------------
/chafenqi/Assets.xcassets/settings_prompt.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "settings_prompt.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/chafenqi/Assets.xcassets/settings_prompt.imageset/settings_prompt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Louiswu2011/chafenqi/206cb81997741633392f743c6c06d7ec754e7e13/chafenqi/Assets.xcassets/settings_prompt.imageset/settings_prompt.png
--------------------------------------------------------------------------------
/chafenqi/Assets.xcassets/switch_mode_prompt.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "switch_mode_prompt.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/chafenqi/Assets.xcassets/switch_mode_prompt.imageset/switch_mode_prompt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Louiswu2011/chafenqi/206cb81997741633392f743c6c06d7ec754e7e13/chafenqi/Assets.xcassets/switch_mode_prompt.imageset/switch_mode_prompt.png
--------------------------------------------------------------------------------
/chafenqi/Assets.xcassets/switch_prompt.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "switch_prompt.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/chafenqi/Assets.xcassets/switch_prompt.imageset/switch_prompt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Louiswu2011/chafenqi/206cb81997741633392f743c6c06d7ec754e7e13/chafenqi/Assets.xcassets/switch_prompt.imageset/switch_prompt.png
--------------------------------------------------------------------------------
/chafenqi/Assets.xcassets/tutorial_prompt.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "tutorial_prompt.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/chafenqi/Assets.xcassets/tutorial_prompt.imageset/tutorial_prompt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Louiswu2011/chafenqi/206cb81997741633392f743c6c06d7ec754e7e13/chafenqi/Assets.xcassets/tutorial_prompt.imageset/tutorial_prompt.png
--------------------------------------------------------------------------------
/chafenqi/CoreData/CacheController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CacheController.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2023/5/8.
6 | //
7 |
8 | import Foundation
9 | import CoreData
10 |
11 | class CacheController: ObservableObject {
12 | static let shared = CacheController()
13 |
14 | var container = NSPersistentContainer(name: "ImageCache")
15 |
16 | init() {
17 | print("[CacheController] Initializing persistent stores...")
18 | container.loadPersistentStores { (storeDescription, error) in
19 | if let error = error as NSError? {
20 | fatalError("[CacheController] Container load failed: \(error)")
21 | }
22 | print("[CacheController] Loaded store: \(storeDescription.description)")
23 | }
24 | print(container.viewContext.name ?? "no name")
25 | }
26 |
27 | func getCacheSize() -> String {
28 | var byteSize = 0
29 | let storeUrls = container.persistentStoreCoordinator.persistentStores.compactMap { $0.url }
30 | for url in storeUrls {
31 | do {
32 | let size = try Data(contentsOf: url)
33 | if (size.count >= 1) {
34 | byteSize += size.count
35 | }
36 | } catch {
37 | print("[CacheController] Failed to calculate cache size: \(error.localizedDescription)")
38 | return "加载出错"
39 | }
40 | }
41 | let bcf = ByteCountFormatter()
42 | bcf.countStyle = .file
43 | if (byteSize == 0) {
44 | return "0 KB"
45 | }
46 | return bcf.string(fromByteCount: Int64(byteSize))
47 | }
48 |
49 | func clearCache() {
50 | do {
51 | let deleteRequests = [NSBatchDeleteRequest(fetchRequest: CoverCache.fetchRequest()), NSBatchDeleteRequest(fetchRequest: ChartCache.fetchRequest())]
52 | for request in deleteRequests {
53 | request.resultType = .resultTypeObjectIDs
54 | let batchDelete = try container.viewContext.execute(request) as? NSBatchDeleteResult
55 | guard let deleteResult = batchDelete?.result as? [NSManagedObjectID] else { fatalError("[CacheController] Type assertion failed.") }
56 | let deletedObjects: [AnyHashable: Any] = [NSDeletedObjectsKey: deleteResult]
57 | NSManagedObjectContext.mergeChanges(fromRemoteContextSave: deletedObjects, into: [container.viewContext])
58 | }
59 | } catch {
60 | print("[CacheController] Failed to purge cache: \(error.localizedDescription)")
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/chafenqi/CoreData/ImageCache.xcdatamodeld/ImageCache.xcdatamodel/contents:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/chafenqi/CoreData/WidgetData.xcdatamodeld/WidgetData.xcdatamodel/contents:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/chafenqi/CoreData/WidgetDataController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WidgetDataController.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2023/6/30.
6 | //
7 |
8 | import Foundation
9 | import CoreData
10 |
11 | class WidgetDataController {
12 | static let shared = WidgetDataController()
13 |
14 | let encoder = JSONEncoder()
15 |
16 | var container = NSPersistentContainer(name: "WidgetData")
17 |
18 | init() {
19 | print("[WidgetDataController] Initializing persistent stores...")
20 | let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.com.nltv.chafenqi.shared")
21 | let storeURL = containerURL?.appendingPathComponent("WidgetData.sqlite")
22 | let description = NSPersistentStoreDescription(url: storeURL!)
23 |
24 | description.shouldMigrateStoreAutomatically = true
25 | description.shouldInferMappingModelAutomatically = true
26 |
27 | container.persistentStoreDescriptions = [description]
28 | container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
29 | container.loadPersistentStores { (storeDescription, error) in
30 | if let error = error as NSError? {
31 | fatalError("[CacheController] Container load failed: \(error)")
32 | }
33 | print("[WidgetDataController] Loaded store: \(storeDescription.description)")
34 | }
35 | }
36 |
37 | func save(data: WidgetData, context: NSManagedObjectContext) throws {
38 | let backgroundContext = container.newBackgroundContext()
39 | try backgroundContext.performAndWait {
40 | let widgetData = WidgetUser(context: self.container.viewContext)
41 | widgetData.username = data.username
42 | widgetData.isPremium = data.isPremium
43 | widgetData.maimai = nil
44 | widgetData.chunithm = nil
45 | widgetData.chuRecentOne = nil
46 | widgetData.maiRecentOne = nil
47 | widgetData.chuChar = nil
48 | widgetData.chuBg = nil
49 | widgetData.maiChar = nil
50 | widgetData.maiBg = nil
51 | widgetData.custom = nil
52 | if let data = data.maimaiInfo {
53 | widgetData.maimai = try encoder.encode(data)
54 | }
55 | if let data = data.chunithmInfo {
56 | widgetData.chunithm = try encoder.encode(data)
57 | }
58 | if let data = data.maiRecentOne {
59 | widgetData.maiRecentOne = try encoder.encode(data)
60 | }
61 | if let data = data.chuRecentOne {
62 | widgetData.chuRecentOne = try encoder.encode(data)
63 | }
64 | if let data = data.custom {
65 | widgetData.custom = try encoder.encode(data)
66 | }
67 | widgetData.maiCover = data.maiCover
68 | widgetData.chuCover = data.chuCover
69 | widgetData.maiBg = data.maiBg
70 | widgetData.chuBg = data.chuBg
71 | widgetData.maiChar = data.maiChar
72 | widgetData.chuChar = data.chuChar
73 |
74 | if backgroundContext.hasChanges {
75 | print("[WidgetDataController] Saving widget data of", widgetData.username ?? "unknown user")
76 | try self.container.viewContext.save()
77 | }
78 | }
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/chafenqi/Extension/FoundationExtension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FoundationExtension.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2023/9/13.
6 | //
7 |
8 | import Foundation
9 | import SwiftUI
10 |
11 | extension String {
12 | var displayRate: String {
13 | return self.replacingOccurrences(of: "p", with: "+").uppercased()
14 | }
15 | }
16 |
17 | extension Array where Element: Equatable {
18 | var unique: [Element] {
19 | var uniqueValues: [Element] = []
20 | forEach { item in
21 | guard !uniqueValues.contains(item) else { return }
22 | uniqueValues.append(item)
23 | }
24 | return uniqueValues
25 | }
26 | }
27 |
28 | extension Array where Element == Double {
29 | func getOrNull(_ index: Int, defaultValue: Double = 0.0) -> Double {
30 | if index >= self.count || index < 0 {
31 | return defaultValue
32 | } else {
33 | return self[index]
34 | }
35 | }
36 | }
37 |
38 | extension Double {
39 | func cut(remainingDigits: Int) -> Double {
40 | return floor(self * pow(10, Double(remainingDigits))) / pow(10, Double(remainingDigits))
41 | }
42 | }
43 |
44 | extension Date {
45 | var startOfMonth: Date {
46 | let calendar = Calendar(identifier: .gregorian)
47 | let components = calendar.dateComponents([.year, .month], from: self)
48 | return calendar.date(from: components)!
49 | }
50 | var endOfMonth: Date {
51 | var components = DateComponents()
52 | components.month = 1
53 | components.second = -1
54 | return Calendar(identifier: .gregorian).date(byAdding: components, to: startOfMonth)!
55 | }
56 |
57 | }
58 |
59 | extension Collection {
60 | // Returns the element at the specified index if it is within bounds, otherwise nil.
61 | subscript(orNil index: Index) -> Element? {
62 | return indices.contains(index) ? self[index] : nil
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/chafenqi/Extension/URLResponseExtension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // URLResponseExtension.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2023/3/7.
6 | //
7 |
8 | import Foundation
9 |
10 | extension URLResponse {
11 | func statusCode() -> Int {
12 | let httpRespnse = self as! HTTPURLResponse
13 | return httpRespnse.statusCode
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/chafenqi/Extension/ViewExtension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewExtension.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2023/9/13.
6 | //
7 |
8 | import Foundation
9 | import SwiftUI
10 |
11 | extension View {
12 | @ViewBuilder func `if`(_ condition: Bool, transform: (Self) -> Content) -> some View {
13 | if condition {
14 | transform(self)
15 | } else {
16 | self
17 | }
18 | }
19 |
20 | @ViewBuilder func iff(_ condition: Bool, trueTransform: (Self) -> Content, falseTransform: (Self) -> Content) -> some View {
21 | if condition {
22 | trueTransform(self)
23 | } else {
24 | falseTransform(self)
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/chafenqi/Helper/AsyncImage/AsyncImage.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AsyncImage.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2023/2/24.
6 | //
7 |
8 | import Foundation
9 | import CoreData
10 | import SwiftUI
11 |
12 | struct AsyncImage: View {
13 | @StateObject private var loader: ImageLoader
14 | private let placeholder: Placeholder
15 | private let image: (UIImage) -> Image
16 |
17 | init(
18 | url: URL,
19 | context: NSManagedObjectContext,
20 | @ViewBuilder placeholder: () -> Placeholder,
21 | @ViewBuilder image: @escaping (UIImage) -> Image = Image.init(uiImage:)
22 | ) {
23 | self.placeholder = placeholder()
24 | self.image = image
25 | _loader = StateObject(wrappedValue: ImageLoader(url: url, context: context))
26 | }
27 |
28 | var body: some View {
29 | content
30 | .onAppear(perform: loader.load)
31 | }
32 |
33 | private var content: some View {
34 | Group {
35 | if loader.image != nil {
36 | image(loader.image!)
37 | } else {
38 | placeholder
39 | .padding()
40 | }
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/chafenqi/Helper/AsyncImage/ImageCache.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ImageCache.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2023/2/24.
6 | //
7 |
8 | import UIKit
9 | import SwiftUI
10 |
11 | struct ImageCacheKey: EnvironmentKey {
12 | static let defaultValue: ImageCache = TemporaryImageCache()
13 | }
14 |
15 | extension EnvironmentValues {
16 | var imageCache: ImageCache {
17 | get { self[ImageCacheKey.self] }
18 | set { self[ImageCacheKey.self] = newValue }
19 | }
20 | }
21 |
22 | protocol ImageCache {
23 | subscript(_ url: URL) -> UIImage? { get set }
24 | }
25 |
26 | struct TemporaryImageCache: ImageCache {
27 | private let cache: NSCache = {
28 | let cache = NSCache()
29 | cache.countLimit = 100 // 100 items
30 | cache.totalCostLimit = 1024 * 1024 * 100 // 100 MB
31 | return cache
32 | }()
33 |
34 | subscript(_ key: URL) -> UIImage? {
35 | get { cache.object(forKey: key as NSURL) }
36 | set { newValue == nil ? cache.removeObject(forKey: key as NSURL) : cache.setObject(newValue!, forKey: key as NSURL) }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/chafenqi/Helper/AsyncImage/ImageLoader.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ImageLoader.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2023/2/24.
6 | //
7 |
8 | import Combine
9 | import UIKit
10 | import SwiftUI
11 | import CoreData
12 |
13 | class ImageLoader: ObservableObject {
14 | @Published var image: UIImage?
15 |
16 | private var viewContext: NSManagedObjectContext
17 |
18 | private(set) var isLoading = false
19 |
20 | @State private var url: URL
21 | private var cancellable: AnyCancellable?
22 |
23 | private static let imageProcessingQueue = DispatchQueue(label: "image-processing")
24 |
25 | init(url: URL, cache: ImageCache? = nil, context: NSManagedObjectContext) {
26 | self.url = url
27 | self.viewContext = context
28 | }
29 |
30 | deinit {
31 | cancel()
32 | }
33 |
34 | func load() {
35 | guard !isLoading else { return }
36 |
37 | let fetchRequest = CoverCache.fetchRequest()
38 | fetchRequest.predicate = NSPredicate(format: "imageUrl == %@", url.absoluteString)
39 | let matches = try? viewContext.fetch(fetchRequest)
40 | if let match = matches?.first?.image {
41 | let img = UIImage(data: match)
42 | self.image = img
43 | isLoading = false
44 | // print("[ImageLoader] Read from cache.")
45 | return
46 | }
47 |
48 | cancellable = URLSession.shared.dataTaskPublisher(for: url)
49 | .map { UIImage(data: $0.data) }
50 | .replaceError(with: nil)
51 | .handleEvents(receiveSubscription: { [weak self] _ in self?.onStart() },
52 | receiveOutput: { [weak self] in self?.saveToCache($0) },
53 | receiveCompletion: { [weak self] _ in self?.onFinish() },
54 | receiveCancel: { [weak self] in self?.onFinish() })
55 | .subscribe(on: Self.imageProcessingQueue)
56 | .receive(on: DispatchQueue.main)
57 | .sink {
58 | [weak self] in self?.image = $0
59 | }
60 | }
61 |
62 | func cancel() {
63 | cancellable?.cancel()
64 | }
65 |
66 | private func onStart() {
67 | isLoading = true
68 | }
69 |
70 | private func onFinish() {
71 | isLoading = false
72 | }
73 |
74 | private func saveToCache(_ image: UIImage?) {
75 | guard let image = image else { return }
76 |
77 | guard let pngData = image.pngData() else { return }
78 | let url = self.url.absoluteString
79 |
80 | let container = CacheController.shared.container
81 |
82 | let backgroundContext = container.newBackgroundContext()
83 | backgroundContext.perform {
84 | let cacheItem = CoverCache(context: backgroundContext)
85 | cacheItem.imageUrl = url
86 | cacheItem.image = pngData
87 |
88 | do {
89 | try backgroundContext.save()
90 | print("[ImageLoader] Saved \(self.url.absoluteString) to cache.")
91 | } catch {
92 | print("[ImageLoader] Failed to save cache: \(error.localizedDescription)")
93 | }
94 | }
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/chafenqi/Helper/ChartIdConverter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ChartImageGrabber.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2023/1/19.
6 | //
7 |
8 | import Foundation
9 |
10 | struct ChartIdConverter {
11 | private var map: Dictionary
12 |
13 | static func getWebChartId(musicId: Int, map: [String: String]) throws -> String {
14 | let id = map["\(musicId)"]
15 | if (id == "Unknown") { throw CFQError.unsupportedError(reason: "World's End charts are not supported right now.") }
16 | return id!
17 | }
18 |
19 | static func getAvailableDiffs(musicId: Int, map: [String: String]) async throws -> [String] {
20 | let diffs = ["exp", "mst", "ult"]
21 | var availableDiffs: [String] = []
22 | let id = map["\(musicId)"]!
23 | if (id == "Unknown") { throw CFQError.unsupportedError(reason: "暂不支持WE谱面预览") }
24 |
25 | for diff in diffs {
26 | let chartURL = URL(string: "https://sdvx.in/chunithm/\(diff == "ult" ? "ult" : id.prefix(2))/obj/data\(id)\(diff).png")
27 | let request = URLRequest(url: chartURL!)
28 | let (_, response) = try await URLSession.shared.data(for: request)
29 |
30 | if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 {
31 | availableDiffs.append({
32 | switch(diff) {
33 | case "exp":
34 | return "Expert"
35 | case "mst":
36 | return "Master"
37 | case "ult":
38 | return "Ultima"
39 | default:
40 | return "Master"
41 | }
42 | }())
43 | }
44 | }
45 |
46 | return availableDiffs
47 | }
48 | }
49 |
50 | extension String {
51 | func toDiffIndex() -> Int {
52 | switch self {
53 | case "Basic": return 0
54 | case "Advanced": return 1
55 | case "Expert": return 2
56 | case "Master": return 3
57 | case "Ultima": return 4
58 | case "World's End": return 5
59 | default: return -1
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/chafenqi/Helper/ChartImageGrabber.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ChartImageGrabber.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2023/1/22.
6 | //
7 |
8 | import Foundation
9 | import SwiftUI
10 | import CoreData
11 | import UIKit
12 |
13 | class ChartImageGrabber: ObservableObject {
14 | func downloadChartImage(musicId: String, diffIndex: Int, context: NSManagedObjectContext) async throws -> UIImage {
15 | let barURL: URL?
16 | let bgURL: URL?
17 | let chartURL: URL?
18 |
19 | barURL = URL(string: "\(CFQServer.serverAddress)api/chunithm/preview?musicId=\(musicId)&diff=\(diffIndex)&type=bar")
20 | bgURL = URL(string: "\(CFQServer.serverAddress)api/chunithm/preview?musicId=\(musicId)&diff=\(diffIndex)&type=bg")
21 | chartURL = URL(string: "\(CFQServer.serverAddress)api/chunithm/preview?musicId=\(musicId)&diff=\(diffIndex)&type=chart")
22 |
23 | let fetchRequest = ChartCache.fetchRequest()
24 | fetchRequest.predicate = NSPredicate(format: "imageUrl == %@", chartURL?.absoluteString ?? "ongeki wen?")
25 | let matches = try? context.fetch(fetchRequest)
26 | if let match = matches?.first?.image {
27 | // print("[ChartImageGrabber] Read from cache.")
28 | return UIImage(data: match)!
29 | }
30 |
31 | do {
32 | async let barImage = try downloadImageFromUrl(url: barURL!, index: 0)
33 | async let bgImage = try downloadImageFromUrl(url: bgURL!, index: 1)
34 | async let chartImage = try downloadImageFromUrl(url: chartURL!, index: 2)
35 |
36 | let images = try await [barImage, bgImage, chartImage]
37 |
38 | UIGraphicsBeginImageContext(images[0].size)
39 |
40 | let areaSize = CGRect(x: 0, y: 0, width: images[0].size.width, height: images[0].size.height)
41 | images[0].draw(in: areaSize)
42 | images[1].draw(in: areaSize, blendMode: .normal, alpha: 1.0)
43 | images[2].draw(in: areaSize, blendMode: .normal, alpha: 1.0)
44 |
45 | let mergedImage = UIGraphicsGetImageFromCurrentImageContext()!
46 | UIGraphicsEndImageContext()
47 |
48 | saveToCache(mergedImage, chartUrl: chartURL?.absoluteString ?? "ongeki wen?", context: context)
49 |
50 | return mergedImage
51 | } catch {
52 | throw CFQError.requestTimeoutError
53 | }
54 | }
55 |
56 | private func downloadImageFromUrl(url: URL, index: Int) async throws -> UIImage {
57 | let request = URLRequest(url: url)
58 | let (data, _) = try await URLSession.shared.data(for: request)
59 |
60 | let image = UIImage(data: data)
61 | if let image = image {
62 | return image
63 | } else {
64 | throw CFQError.BadRequestError
65 | }
66 | }
67 |
68 | private func saveToCache(_ image: UIImage, chartUrl: String, context: NSManagedObjectContext) {
69 | do {
70 | let chartCache = ChartCache(context: context)
71 | chartCache.image = image.pngData()!
72 | chartCache.imageUrl = chartUrl
73 | try context.save()
74 | print("[ChartImageGrabber] Saved \(chartUrl) to cache.")
75 | } catch {
76 | print("[ChartImageGrabber] Failed to save cache: \(error.localizedDescription)")
77 | }
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/chafenqi/Helper/ChunithmDataGrabber.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ProberDataGrabber.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2023/1/8.
6 | //
7 |
8 | import Foundation
9 |
10 | struct ChunithmDataGrabber {
11 | static func getSongCoverUrl(source: Int, musicId: String) -> URL {
12 | return URL(string: "\(SharedValues.apiServerAddress)api/resource/chunithm/cover?musicId=\(musicId)")!
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/chafenqi/Helper/DataTool.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DataTool.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2023/1/21.
6 | //
7 |
8 | import Foundation
9 |
10 | let teamNameLimit = 24
11 | let teamStyleLimit = 16
12 | let teamRemarksLimit = 120
13 |
14 | let difficulty = ["Expert": "exp", "Master": "mst", "Ultima": "ult"]
15 | let chunithmRanks = ["SSS+", "SSS", "SS+", "SS", "S+", "S", "其他"]
16 |
17 | let maimaiLevelLabel = [
18 | 0: "Basic",
19 | 1: "Advanced",
20 | 2: "Expert",
21 | 3: "Master",
22 | 4: "Re:Master"
23 | ]
24 |
25 | let chunithmLevelLabel = [
26 | 0: "Basic",
27 | 1: "Advanced",
28 | 2: "Expert",
29 | 3: "Master",
30 | 4: "Ultima",
31 | 5: "World's End"
32 | ]
33 |
34 | class DataTool {
35 | static let shared = DataTool()
36 |
37 | let numberFormatter = NumberFormatter()
38 |
39 | init() {
40 | numberFormatter.maximumFractionDigits = 2
41 | numberFormatter.numberStyle = .decimal
42 | numberFormatter.roundingMode = .down
43 | }
44 | }
45 |
46 |
47 | func getCoverNumber(id: String) -> String {
48 | if let id = Int(id) {
49 | if id >= 100000 {
50 | return getCoverNumber(id: String(id - 100000))
51 | } else if (10000...11000).contains(id) {
52 | let pad = id - 10000
53 | var padded = String(pad)
54 | while padded.count < 5 {
55 | padded = "0" + padded
56 | }
57 | return padded
58 | } else if id < 10000 {
59 | var padded = String(id)
60 | while padded.count < 5 {
61 | padded = "0" + padded
62 | }
63 | return padded
64 | } else {
65 | return String(id)
66 | }
67 | } else {
68 | return id
69 | }
70 | }
71 |
72 |
--------------------------------------------------------------------------------
/chafenqi/Helper/MaimaiDataGrabber.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MaimaiDataGrabber.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2023/2/3.
6 | //
7 |
8 | import Foundation
9 |
10 | struct MaimaiDataGrabber {
11 | static func getSongCoverUrl(source: Int, coverId: Int) -> URL {
12 | return URL(string: "https://assets2.lxns.net/maimai/jacket/\(coverId).png")!
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/chafenqi/Helper/ModalView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ModalView.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2023/2/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct ModalView: UIViewControllerRepresentable {
11 | let view: T
12 | let isModal: Bool
13 | let onDismissalAttempt: (()->())?
14 |
15 | func makeUIViewController(context: Context) -> UIHostingController {
16 | UIHostingController(rootView: view)
17 | }
18 |
19 | func updateUIViewController(_ uiViewController: UIHostingController, context: Context) {
20 | context.coordinator.modalView = self
21 | uiViewController.rootView = view
22 | uiViewController.parent?.presentationController?.delegate = context.coordinator
23 | }
24 |
25 | func makeCoordinator() -> Coordinator {
26 | Coordinator(self)
27 | }
28 |
29 | class Coordinator: NSObject, UIAdaptivePresentationControllerDelegate {
30 | var modalView: ModalView
31 |
32 | init(_ modalView: ModalView) {
33 | self.modalView = modalView
34 | }
35 |
36 | func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool {
37 | !modalView.isModal
38 | }
39 |
40 | func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) {
41 | modalView.onDismissalAttempt?()
42 | }
43 | }
44 | }
45 |
46 | extension View {
47 | func presentation(isModal: Bool, onDismissalAttempt: (()->())? = nil) -> some View {
48 | ModalView(view: self, isModal: isModal, onDismissalAttempt: onDismissalAttempt)
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/chafenqi/Helper/ScreenshotMaker.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ScreenshotMaker.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2023/8/3.
6 | //
7 |
8 | import Foundation
9 | import CoreData
10 | import SwiftUI
11 | import UIKit
12 |
13 | typealias ScreenshotMakerClosure = (ScreenshotMaker) -> Void
14 |
15 | struct ScreenshotMakerView: UIViewRepresentable {
16 | let closure: ScreenshotMakerClosure
17 |
18 | init(_ closure: @escaping ScreenshotMakerClosure) {
19 | self.closure = closure
20 | }
21 |
22 | func makeUIView(context: Context) -> ScreenshotMaker {
23 | let view = ScreenshotMaker(frame: .zero)
24 | return view
25 | }
26 |
27 | func updateUIView(_ uiView: ScreenshotMaker, context: Context) {
28 | DispatchQueue.main.async {
29 | closure(uiView)
30 | }
31 | }
32 | }
33 |
34 | class ScreenshotMaker: UIView {
35 | func screenshot() -> UIImage? {
36 | guard let containerView = self.superview?.superview,
37 | let containerSuperview = containerView.superview else { return nil }
38 | let renderer = UIGraphicsImageRenderer(bounds: containerView.frame)
39 | return renderer.image { (context) in
40 | containerSuperview.backgroundColor = .white
41 | containerSuperview.layer.render(in: context.cgContext)
42 | }
43 | }
44 | }
45 |
46 | extension View {
47 | func snapshotSelf() -> UIImage {
48 | return snapshot(self)
49 | }
50 |
51 | func snapshotWithContext(_ context: NSManagedObjectContext) -> UIImage {
52 | return snapshot(self.environment(\.managedObjectContext, context))
53 | }
54 |
55 | func snapshot(_ view: some View) -> UIImage {
56 | let controller = UIHostingController(rootView: view)
57 | let view = controller.view
58 |
59 | let targetSize = controller.view.intrinsicContentSize
60 | let bounds = CGRect(origin: .zero, size: targetSize)
61 |
62 | view?.bounds = bounds
63 | view?.backgroundColor = .clear
64 |
65 | let window = UIWindow(frame: bounds)
66 | window.rootViewController = controller
67 | window.makeKeyAndVisible()
68 |
69 | let image = controller.view.asImage()
70 | controller.view.removeFromSuperview()
71 | return image
72 | }
73 |
74 | func screenshotView(_ closure: @escaping ScreenshotMakerClosure) -> some View {
75 | let screenshotView = ScreenshotMakerView(closure)
76 | return overlay(screenshotView.allowsHitTesting(false))
77 | }
78 | }
79 |
80 | extension UIView {
81 | func asImage() -> UIImage {
82 | let renderer = UIGraphicsImageRenderer(bounds: bounds)
83 | return renderer.image { context in
84 | layer.render(in: context.cgContext)
85 | }
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/chafenqi/Helper/ShareSheetMaker.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ShareSheetMaker.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2023/8/6.
6 | //
7 |
8 | import Foundation
9 | import UIKit
10 | import SwiftUI
11 |
12 | struct ActivityViewController: UIViewControllerRepresentable {
13 | var activityItems: [Any]
14 | var applicationActivities: [UIActivity]? = nil
15 |
16 | func makeUIViewController(context: UIViewControllerRepresentableContext) -> UIActivityViewController {
17 | let controller = UIActivityViewController(activityItems: activityItems, applicationActivities: applicationActivities)
18 | return controller
19 | }
20 |
21 | func updateUIViewController(_ uiViewController: UIActivityViewController, context: UIViewControllerRepresentableContext) {}
22 | }
23 |
--------------------------------------------------------------------------------
/chafenqi/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleURLTypes
6 |
7 |
8 | CFBundleTypeRole
9 | Editor
10 | CFBundleURLIconFile
11 |
12 | CFBundleURLName
13 | com.nltv.chafenqi
14 | CFBundleURLSchemes
15 |
16 | chafenqi
17 |
18 |
19 |
20 | FirebaseAppDelegateProxyEnabled
21 |
22 | FirebaseAutomaticScreenReportingEnabled
23 |
24 | ITSAppUsesNonExemptEncryption
25 |
26 | NSAppTransportSecurity
27 |
28 | NSExceptionDomains
29 |
30 | 43.139.107.206
31 |
32 | NSExceptionAllowsInsecureHTTPLoads
33 |
34 | NSIncludesSubdomains
35 |
36 |
37 |
38 |
39 | NSUserActivityTypes
40 |
41 | BuildProxyURLIntent
42 | ConfigurationIntent
43 | FetchFishTokenIntent
44 | StartProxyIntent
45 | StopProxyIntent
46 |
47 | UIApplicationShortcutItems
48 |
49 |
50 | UIApplicationShortcutItemIconSymbolName
51 | paperplane.fill
52 | UIApplicationShortcutItemTitle
53 | 一键传分
54 | UIApplicationShortcutItemType
55 | OneClickUpload
56 |
57 |
58 | UIBackgroundModes
59 |
60 | remote-notification
61 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/chafenqi/Object/AlertToastManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AlertToastManager.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2023/2/13.
6 | //
7 |
8 | import Foundation
9 |
10 | final class AlertToastManager: ObservableObject {
11 | @Published var showingUpdaterPasted = false
12 | @Published var showingTutorialReseted = false
13 | @Published var showingRecordDeleted = false
14 | @Published var showingCommentPostSucceed = false
15 | @Published var showingCommentPostFailed = false
16 |
17 | static let shared = AlertToastManager()
18 | }
19 |
--------------------------------------------------------------------------------
/chafenqi/Object/AlertToastModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AlertToastModel.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2023/5/6.
6 | //
7 |
8 | import Foundation
9 | import SwiftUI
10 | import AlertToast
11 |
12 | class AlertToastModel: ObservableObject {
13 | @Published var show = false
14 |
15 | @Published var toast = AlertToast(displayMode: .hud, type: .regular){
16 | didSet {
17 | show.toggle()
18 | }
19 | }
20 |
21 | @Published var alertShow = false
22 | @Published var alert = Alert(title: Text("")) {
23 | didSet {
24 | alertShow.toggle()
25 | }
26 | }
27 |
28 | static var shared = AlertToastModel()
29 | }
30 |
--------------------------------------------------------------------------------
/chafenqi/Object/RegexManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RegexManager.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2023/3/6.
6 | //
7 |
8 | import Foundation
9 |
10 |
11 | final class RegexManager {
12 | static let shared = RegexManager()
13 |
14 | let constantRegex = try! NSRegularExpression(pattern: #"\[(?[0-9]{1,2})(?\.[0-9])?-(?[0-9]{1,2})(?\.[0-9])?\]"#)
15 | let levelRegex = try! NSRegularExpression(pattern: #"<(?[0-9]{1,2}[+]?)-(?[0-9]{1,2}[+]?)>"#)
16 | let difficultyRegex = try! NSRegularExpression(pattern: #"\{[0-4]{1}\}"#)
17 | }
18 |
--------------------------------------------------------------------------------
/chafenqi/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/chafenqi/Resource/ChafenqiColor.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ChafenqiColor.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2023/2/25.
6 | //
7 |
8 | import Foundation
9 | import SwiftUI
10 |
11 | struct ChafenqiColor {
12 | static let accent = Color(red: 241, green: 103, blue: 103)
13 | static let secondary = Color(red: 255, green: 184, blue: 76)
14 | static let auxilary = Color(red: 164, green: 89, blue: 209)
15 | static let background = Color(red: 245, green: 234, blue: 234)
16 | }
17 |
18 |
19 | extension Color {
20 | init(red: Int, green: Int, blue: Int){
21 | self.init(red: Double(red) / 255, green: Double(green) / 255, blue: Double(blue) / 255)
22 | }
23 | }
24 |
25 | let maimaiLevelColor = [
26 | 0: Color(red: 128, green: 216, blue: 98),
27 | 1: Color(red: 242, green: 218, blue: 71),
28 | 2: Color(red: 237, green: 127, blue: 132),
29 | 3: Color(red: 176, green: 122, blue: 238),
30 | 4: Color(red: 206, green: 164, blue: 251)
31 | ]
32 |
33 | let chunithmLevelColor = [
34 | 0: Color(red: 73, green: 166, blue: 137),
35 | 1: Color(red: 237, green: 123, blue: 33),
36 | 2: Color(red: 205, green: 85, blue: 77),
37 | 3: Color(red: 171, green: 104, blue: 249),
38 | 4: Color(red: 129, green: 133, blue: 137),
39 | 5: Color.white
40 | ]
41 |
42 | let chunithmRankColor = [
43 | 0: Color(hex: 0xffffa800),
44 | 1: Color(hex: 0xffca8402),
45 | 2: Color(hex: 0xff78ce95),
46 | 3: Color(hex: 0xff369656),
47 | 4: Color(hex: 0xfffe2f20),
48 | 5: Color(hex: 0xff8e0a01),
49 | 6: Color(hex: 0xff818589)
50 | ]
51 |
52 | let nameplateDefaultChuniColorTop = Color(red: 254, green: 241, blue: 65)
53 | let nameplateDefaultChuniColorBottom = Color(red: 243, green: 200, blue: 48)
54 |
55 | let nameplateThemedChuniColors = [
56 | Color(red: 192, green: 230, blue: 249),
57 | Color(red: 219, green: 226, blue: 250),
58 | Color(red: 240, green: 223, blue: 246),
59 | Color(red: 248, green: 211, blue: 238),
60 | Color(red: 245, green: 178, blue: 225)
61 | ]
62 | let nameplateThemedMaiColors = [
63 | Color(red: 235, green: 182, blue: 85),
64 | Color(red: 235, green: 187, blue: 87),
65 | Color(red: 236, green: 196, blue: 90),
66 | Color(red: 235, green: 200, blue: 89),
67 | Color(red: 242, green: 225, blue: 68)
68 | ]
69 |
70 | let nameplateDefaultMaiColorTop = Color(red: 167, green: 243, blue: 254)
71 | let nameplateDefaultMaiColorBottom = Color(red: 93, green: 166, blue: 247)
72 |
73 | let nameplateDefaultChuniGradientStyle = LinearGradient(colors: [nameplateDefaultChuniColorTop, nameplateDefaultChuniColorBottom], startPoint: .top, endPoint: .bottom)
74 | let nameplateDefaultMaiGradientStyle = LinearGradient(colors: [nameplateDefaultMaiColorTop, nameplateDefaultMaiColorBottom], startPoint: .top, endPoint: .bottom)
75 |
76 | let nameplateThemedChuniGradientStyle = LinearGradient(colors: nameplateThemedChuniColors, startPoint: .topLeading, endPoint: .bottomTrailing)
77 | let nameplateThemedMaiGradientStyle = LinearGradient(colors: nameplateThemedMaiColors, startPoint: .topLeading, endPoint: .bottomTrailing)
78 |
79 | let leaderboardGoldColor = Color(hex: 0xaf9500)
80 | let leaderboardSilverColor = Color(hex: 0xb4b4b4)
81 | let leaderboardBronzeColor = Color(hex: 0x6a3805)
82 |
--------------------------------------------------------------------------------
/chafenqi/Resource/dlc.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": 2021,
4 | "title": "拝啓、桜舞い散るこの日に",
5 | "ds": [
6 | 3, 6, 9.5, 12.5
7 | ],
8 | "level": [
9 | "3", "6", "9+", "12+"
10 | ],
11 | "cids": [
12 | 9999, 9998, 9997, 9996
13 | ],
14 | "charts": [
15 | { "combo": 576, "charter": "-" },
16 | { "combo": 825, "charter": "-" },
17 | { "combo": 1056, "charter": "アミノハバキリ" },
18 | { "combo": 1539, "charter": "Redarrow" }
19 | ],
20 | "basic_info": {
21 | "title": "拝啓、桜舞い散るこの日に",
22 | "artist": "まふまふ",
23 | "genre": "niconico",
24 | "bpm": 186,
25 | "from": "CHUNITHM NEW"
26 | }
27 | },
28 | {
29 | "id": 1054,
30 | "title": "サクリファイス",
31 | "ds": [
32 | 3, 7, 9.5, 13.4
33 | ],
34 | "level": [
35 | "3", "7", "9+", "13"
36 | ],
37 | "cids": [
38 | 9995, 9994, 9993, 9992
39 | ],
40 | "charts": [
41 | { "combo": 633, "charter": "-" },
42 | { "combo": 916, "charter": "-" },
43 | { "combo": 1303, "charter": "Redarrow" },
44 | { "combo": 1898, "charter": "écologie" }
45 | ],
46 | "basic_info": {
47 | "title": "サクリファイス",
48 | "artist": "まふまふ",
49 | "genre": "niconico",
50 | "bpm": 192,
51 | "from": "CHUNITHM PARADISE LOST"
52 | }
53 | }
54 | ]
--------------------------------------------------------------------------------
/chafenqi/Service/QuickActionService.swift:
--------------------------------------------------------------------------------
1 | //
2 | // QuickActionService.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2023/4/18.
6 | //
7 |
8 | import Foundation
9 | import UIKit
10 |
11 | enum QuickActionType: String {
12 | case oneClickUpload = "OneClickUpload"
13 | }
14 |
15 | enum QuickAction: Equatable {
16 | case oneClickUpload
17 |
18 | init?(item: UIApplicationShortcutItem) {
19 | guard let type = QuickActionType(rawValue: item.type) else {
20 | return nil
21 | }
22 |
23 | switch type {
24 | case .oneClickUpload:
25 | self = .oneClickUpload
26 | }
27 | }
28 | }
29 |
30 | class QuickActionService: ObservableObject {
31 | static let shared = QuickActionService()
32 |
33 | @Published var action: QuickAction?
34 | }
35 |
--------------------------------------------------------------------------------
/chafenqi/Struct/CFQRemoteOptions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CFQRemoteOptions.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2024/08/29.
6 | //
7 |
8 | import Foundation
9 |
10 | class CFQRemoteOptions {
11 | var authToken: String = ""
12 |
13 | var bindQQ: String = ""
14 | var fishToken: String = ""
15 | var forwardToFish: Bool = false
16 | var forwardToLxns: Bool = false
17 | var rateLimiting: Bool = false
18 | var maimaiFavList: String = ""
19 | var chunithmFavList: String = ""
20 |
21 | func sync(authToken: String) async {
22 | self.authToken = authToken
23 |
24 | bindQQ = await CFQUserServer.getBindQQ(authToken: authToken)
25 | forwardToFish = await CFQUserServer.fetchUserOption(authToken: authToken, param: "forwarding_fish") == "true"
26 | forwardToLxns = await CFQUserServer.fetchUserOption(authToken: authToken, param: "forwarding_lxns") == "true"
27 | rateLimiting = await CFQUserServer.fetchUserOption(authToken: authToken, param: "rate_limiting") == "true"
28 | maimaiFavList = await CFQUserServer.fetchUserOption(authToken: authToken, param: "maimai_fav_list")
29 | chunithmFavList = await CFQUserServer.fetchUserOption(authToken: authToken, param: "chunithm_fav_list")
30 | fishToken = await CFQUserServer.fetchUserOption(authToken: authToken, param: "fish_token")
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/chafenqi/Struct/CFQTeam.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CFQTeam.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2024/12/25.
6 | //
7 |
8 | import Foundation
9 |
10 | class CFQTeam: ObservableObject {
11 | @Published var isLoading: Bool = false
12 |
13 | @Published var currentTeamId: Int? = nil
14 | @Published var current: TeamInfo = TeamInfo.empty
15 |
16 | @Published var list: [TeamBasicInfo] = []
17 | @Published var sortedList: [TeamBasicInfo] = []
18 |
19 | func refresh(user: CFQNUser) {
20 | isLoading = true
21 | Task {
22 | let currentTeamId = await CFQTeamServer.fetchCurrentTeam(authToken: user.jwtToken, game: user.currentMode)
23 | let allTeams = await fetchAllTeams(token: user.jwtToken, mode: user.currentMode)
24 | if let currentTeamId = currentTeamId {
25 | let currentTeam = await CFQTeamServer.fetchTeamInfo(authToken: user.jwtToken, game: user.currentMode, teamId: currentTeamId)
26 | if let currentTeam = currentTeam {
27 | DispatchQueue.main.async {
28 | self.currentTeamId = currentTeamId
29 | self.current = currentTeam
30 | }
31 | }
32 | } else {
33 | DispatchQueue.main.async {
34 | self.current = TeamInfo.empty
35 | }
36 | }
37 | DispatchQueue.main.async {
38 | self.list = allTeams
39 | self.sortedList = allTeams.sorted { $0.currentActivityPoints > $1.currentActivityPoints }
40 | self.isLoading = false
41 | }
42 | }
43 | }
44 |
45 | func fetchAllTeams(token: String, mode: Int) async -> [TeamBasicInfo] {
46 | return await CFQTeamServer.fetchAllTeamInfos(authToken: token, game: mode)
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/chafenqi/Struct/Chunithm/ChunithmLeaderboardEntry.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ChunithmLeaderboardEntry.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2024/06/29.
6 | //
7 |
8 | import Foundation
9 |
10 | struct ChunithmRatingLeaderboardEntry: Codable {
11 | var uid: Int = 0
12 | var username: String = ""
13 | var nickname: String = ""
14 | var rating: Double = 0.0
15 | }
16 | struct ChunithmTotalScoreLeaderboardEntry: Codable {
17 | var uid: Int = 0
18 | var username: String = ""
19 | var nickname: String = ""
20 | var totalScore: Int = 0
21 | }
22 | struct ChunithmTotalPlayedLeaderboardEntry: Codable {
23 | var uid: Int = 0
24 | var username: String = ""
25 | var nickname: String = ""
26 | var totalPlayed: Int = 0
27 | }
28 | struct ChunithmFirstLeaderboardEntry: Codable {
29 | var uid: Int = 0
30 | var username: String = ""
31 | var nickname: String = ""
32 | var firstCount: Int = 0
33 | }
34 | struct ChunithmFirstLeaderboardMusicEntry: Codable {
35 | var musicId: Int = 0
36 | var diffIndex: Int = 0
37 | var score: Int = 0
38 | }
39 |
40 | struct ChunithmRatingRank: Codable {
41 | var uid: Int = 0
42 | var username: String = ""
43 | var nickname: String = ""
44 | var rating: Double = 0.0
45 | var rank: Int = 0
46 | }
47 | struct ChunithmTotalScoreRank: Codable {
48 | var uid: Int = 0
49 | var username: String = ""
50 | var nickname: String = ""
51 | var totalScore: Int = 0
52 | var rank: Int = 0
53 | }
54 | struct ChunithmTotalPlayedRank: Codable {
55 | var uid: Int = 0
56 | var username: String = ""
57 | var nickname: String = ""
58 | var totalPlayed: Int = 0
59 | var rank: Int = 0
60 | }
61 | struct ChunithmFirstRank: Codable {
62 | var rank: Int = 0
63 | var firstCount: Int = 0
64 | }
65 |
66 | typealias ChunithmRatingLeaderboard = [ChunithmRatingLeaderboardEntry]
67 | typealias ChunithmTotalScoreLeaderboard = [ChunithmTotalScoreLeaderboardEntry]
68 | typealias ChunithmTotalPlayedLeaderboard = [ChunithmTotalPlayedLeaderboardEntry]
69 | typealias ChunithmFirstLeaderboard = [ChunithmFirstLeaderboardEntry]
70 |
71 | extension Array {
72 | func getFirstPerDifficulty() -> [Int] {
73 | // Basic, Advanced, Expert, Master, Ultima
74 | var diffCount = [0, 0, 0, 0, 0]
75 | self.forEach { music in
76 | if music.diffIndex <= 4 && music.diffIndex >= 0 {
77 | diffCount[music.diffIndex] += 1
78 | }
79 | }
80 | return diffCount
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/chafenqi/Struct/Comment/UserComment.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UserComment.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2023/8/15.
6 | //
7 |
8 | import Foundation
9 |
10 | struct UserComment: Codable {
11 | static let shared = UserComment(id: 0, timestamp: 0, userId: 1, username: "Admin", content: "Test", gameType: 1, likes: 0, replyId: -1)
12 |
13 | var id: Int
14 | var timestamp: Int
15 | var userId: Int
16 | var username: String
17 | var content: String
18 | var gameType: Int
19 | var likes: Int
20 | var replyId: Int
21 | }
22 |
23 | extension UserComment {
24 | var dateString: String {
25 | DateTool.shared.intTransformer.string(from: Date(timeIntervalSince1970: Double(self.timestamp)))
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/chafenqi/Struct/CustomAlert.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CustomAlert.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2023/4/14.
6 | //
7 |
8 | import Foundation
9 | import SwiftUI
10 |
11 | struct CustomAlert: View {
12 | @Environment(\.presentationMode) var presentation
13 | let message: String
14 | let titlesAndActions: [(title: String, action: (() -> Void)?)] // = [.default(Text("OK"))]
15 |
16 | var body: some View {
17 | VStack {
18 | Text(message)
19 | Divider().padding([.leading, .trailing], 40)
20 | HStack {
21 | ForEach(titlesAndActions.indices, id: \.self) { i in
22 | Button(self.titlesAndActions[i].title) {
23 | (self.titlesAndActions[i].action ?? {})()
24 | self.presentation.wrappedValue.dismiss()
25 | }
26 | .padding()
27 | }
28 | }
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/chafenqi/Struct/Filter/CFQFilterOptions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CFQFilterOptions.swift
3 | // chafenqi
4 | //
5 | // Created by xinyue on 2023/5/29.
6 | //
7 |
8 | import Foundation
9 | import SwiftUI
10 |
11 | enum CFQSortKey: String, CaseIterable, Identifiable, Hashable {
12 | var id: Self {
13 | return self
14 | }
15 |
16 | case level = "等级"
17 | case constant = "定数"
18 | case bpm = "BPM"
19 | }
20 |
21 | enum CFQMaimaiSortDifficulty: String, CaseIterable, Identifiable, Hashable {
22 | var id: Self {
23 | return self
24 | }
25 |
26 | case basic = "Basic"
27 | case advanced = "Advanced"
28 | case expert = "Expert"
29 | case master = "Master"
30 | case remaster = "Re:Master"
31 | }
32 |
33 | enum CFQChunithmSortDifficulty: String, CaseIterable, Identifiable, Hashable {
34 | var id: Self {
35 | return self
36 | }
37 |
38 | case basic = "Basic"
39 | case advanced = "Advanced"
40 | case expert = "Expert"
41 | case master = "Master"
42 | case ultima = "Ultima"
43 | }
44 |
45 | enum CFQSortMethod: String, CaseIterable, Identifiable, Hashable {
46 | var id: Self {
47 | return self
48 | }
49 |
50 | case ascent = "升序"
51 | case descent = "降序"
52 | case random = "乱序"
53 | }
54 |
55 | struct SongListFilterOptions: Equatable {
56 | var versionSelection: [String] = []
57 | var levelSelection: [String] = []
58 | var genreSelection: [String] = []
59 |
60 | var sortEnabled: Bool = false
61 | var sortOrientation: CFQSortMethod = .descent
62 | var sortBy: CFQSortKey = .level
63 | var sortDifficulty: Int = 0
64 |
65 | var hideNotPlayed: Bool = false
66 | var hideUtage: Bool = true
67 | var hideWorldsEnd: Bool = true
68 |
69 | var onlyShowLoved: Bool = false
70 | }
71 |
--------------------------------------------------------------------------------
/chafenqi/Struct/Maimai/MaimaiChartStat.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MaimaiChartStat.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2023/2/3.
6 | //
7 |
8 | import Foundation
9 |
10 | struct MaimaiChartStatWrapper: Codable {
11 | var charts: Dictionary>
12 | }
13 |
14 | struct MaimaiChartStat: Codable {
15 | var playCount: Int?
16 | var diff: String?
17 | var fit_diff: Double?
18 | var averageScore: Double?
19 | var avg: Double?
20 | var avg_dx: Double?
21 | var std_dev: Double?
22 | var dist: Array?
23 | var fc_dist: Array?
24 |
25 | enum CodingKeys: String, CodingKey {
26 | case playCount = "cnt"
27 | case averageScore = "avg"
28 | case diff, fit_diff, avg_dx, dist, fc_dist, std_dev
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/chafenqi/Struct/Maimai/MaimaiGenreData.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MaimaiGenreData.swift
3 | // chafenqi
4 | //
5 | // Created by Louis Wu on 2025/01/26.
6 | //
7 |
8 | import Foundation
9 |
10 | struct MaimaiGenreData: Codable {
11 | var id: Int = 0
12 | var title: String = ""
13 | var genre: String = ""
14 | }
15 |
--------------------------------------------------------------------------------
/chafenqi/Struct/Maimai/MaimaiLeaderboardEntry.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MaimaiLeaderboardEntry.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2024/06/29.
6 | //
7 |
8 | import Foundation
9 |
10 | struct MaimaiRatingLeaderboardEntry: Codable {
11 | var uid: Int = 0
12 | var username: String = ""
13 | var nickname: String = ""
14 | var rating: Int = 0
15 | }
16 | struct MaimaiTotalScoreLeaderboardEntry: Codable {
17 | var uid: Int = 0
18 | var username: String = ""
19 | var nickname: String = ""
20 | var totalAchievements: Double = 0.0
21 | }
22 | struct MaimaiTotalPlayedLeaderboardEntry: Codable {
23 | var uid: Int = 0
24 | var username: String = ""
25 | var nickname: String = ""
26 | var totalPlayed: Int = 0
27 | }
28 | struct MaimaiFirstLeaderboardEntry: Codable {
29 | var uid: Int = 0
30 | var username: String = ""
31 | var nickname: String = ""
32 | var firstCount: Int = 0
33 | }
34 | struct MaimaiFirstLeaderboardMusicEntry: Codable {
35 | var musicId: Int = 0
36 | var diffIndex: Int = 0
37 | var achievements: Double = 0.0
38 | }
39 |
40 | struct MaimaiRatingRank: Codable {
41 | var uid: Int = 0
42 | var username: String = ""
43 | var nickname: String = ""
44 | var rating: Int = 0
45 | var rank: Int = 0
46 | }
47 | struct MaimaiTotalScoreRank: Codable {
48 | var uid: Int = 0
49 | var username: String = ""
50 | var nickname: String = ""
51 | var totalAchievements: Double = 0.0
52 | var rank: Int = 0
53 | }
54 | struct MaimaiTotalPlayedRank: Codable {
55 | var uid: Int = 0
56 | var username: String = ""
57 | var nickname: String = ""
58 | var totalPlayed: Int = 0
59 | var rank: Int = 0
60 | }
61 | struct MaimaiFirstRank: Codable {
62 | var rank: Int = 0
63 | var firstCount: Int = 0
64 | }
65 |
66 | typealias MaimaiRatingLeaderboard = [MaimaiRatingLeaderboardEntry]
67 | typealias MaimaiTotalScoreLeaderboard = [MaimaiTotalScoreLeaderboardEntry]
68 | typealias MaimaiTotalPlayedLeaderboard = [MaimaiTotalPlayedLeaderboardEntry]
69 | typealias MaimaiFirstLeaderboard = [MaimaiFirstLeaderboardEntry]
70 |
71 | extension Array {
72 | func getFirstPerDifficulty() -> [Int] {
73 | // Basic, Advanced, Expert, Master, Re:Master
74 | var diffCount = [0, 0, 0, 0, 0]
75 | self.forEach { music in
76 | if music.diffIndex <= 4 && music.diffIndex >= 0 {
77 | diffCount[music.diffIndex] += 1
78 | }
79 | }
80 | return diffCount
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/chafenqi/Struct/Maimai/MaimaiVersionData.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MaimaiVersionData.swift
3 | // chafenqi
4 | //
5 | // Created by Louis Wu on 2025/01/26.
6 | //
7 |
8 | import Foundation
9 |
10 | struct MaimaiVersionData: Codable {
11 | var id: Int = 0
12 | var title: String = ""
13 | var version: Int = 0
14 | }
15 |
--------------------------------------------------------------------------------
/chafenqi/Struct/Maimai/chafenqi 2023-02-11 15-48-57/ExportOptions.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | destination
6 | upload
7 | manageAppVersionAndBuildNumber
8 |
9 | method
10 | app-store
11 | signingStyle
12 | automatic
13 | stripSwiftSymbols
14 |
15 | teamID
16 | CYVRCL87U4
17 | uploadSymbols
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/chafenqi/Struct/Maimai/chafenqi 2023-02-11 15-48-57/chafenqi.ipa:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Louiswu2011/chafenqi/206cb81997741633392f743c6c06d7ec754e7e13/chafenqi/Struct/Maimai/chafenqi 2023-02-11 15-48-57/chafenqi.ipa
--------------------------------------------------------------------------------
/chafenqi/Struct/Server/CFQMusicStat.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CFQMusicStat.swift
3 | // chafenqi
4 | //
5 | // Created by Louis Wu on 2025/01/26.
6 | //
7 |
8 | import Foundation
9 |
10 | struct CFQMusicStat: Codable {
11 | var musicId: Int = 0
12 | var difficulty: Int = 0
13 | var totalPlayed: Int = 0
14 | var totalFullCombo: Int = 0
15 | var totalAllJustice: Int = 0
16 | var totalFullChain: Int = 0
17 | var totalScore: Double = 0.0
18 | var ssspSplit: Int = 0
19 | var sssSplit: Int = 0
20 | var sspSplit: Int = 0
21 | var ssSplit: Int = 0
22 | var spSplit: Int = 0
23 | var sSplit: Int = 0
24 | var otherSplit: Int = 0
25 | var highestScore: Double = 0.0
26 | var highestPlayer: String = ""
27 | var highestPlayerNickname: String = ""
28 | }
29 |
--------------------------------------------------------------------------------
/chafenqi/Struct/Server/CFQUserInfo.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CFQUserInfo.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2024/12/05.
6 | //
7 |
8 | import Foundation
9 |
10 | struct CFQUserInfo: Codable {
11 | var id: Int
12 | var username: String
13 | var password: String
14 | var premiumUntil: Int
15 | var bindQQ: String
16 | var createdAt: Int
17 | var lastLogin: Int
18 | }
19 |
--------------------------------------------------------------------------------
/chafenqi/Struct/Server/CFQUserUploadStatus.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CFQUserUploadStatus.swift
3 | // chafenqi
4 | //
5 | // Created by Louis Wu on 2025/01/26.
6 | //
7 |
8 | import Foundation
9 |
10 | struct CFQUserUploadStatus: Codable {
11 | var chunithm: Int
12 | var maimai: Int
13 |
14 | enum CodingKeys: String, CodingKey {
15 | case chunithm = "chu"
16 | case maimai = "mai"
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/chafenqi/Struct/Server/Chunithm/UserChunithmEntry.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UserChunithmEntry.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2024/12/24.
6 | //
7 |
8 | import Foundation
9 |
10 | struct UserChunithmBestScoreEntry: Codable, Hashable, Equatable {
11 | let musicId: Int
12 | let levelIndex: Int
13 | let score: Int
14 | let rankIndex: Int
15 | let clearStatus: String
16 | let judgeStatus: String
17 | let chainStatus: String
18 | let lastModified: Int
19 |
20 | var associatedSong: ChunithmMusicData?
21 | }
22 |
23 | struct UserChunithmRecentScoreEntry: Codable, Hashable, Equatable {
24 | let timestamp: Int
25 | let musicId: Int
26 | let difficulty: String
27 | let score: Int
28 | let newRecord: Bool
29 | let judgeCritical: Int
30 | let judgeJustice: Int
31 | let judgeAttack: Int
32 | let judgeMiss: Int
33 | let noteTap: String
34 | let noteHold: String
35 | let noteSlide: String
36 | let noteAir: String
37 | let noteFlick: String
38 | let rankIndex: Int
39 | let clearStatus: String
40 | let judgeStatus: String
41 | let chainStatus: String
42 |
43 | var associatedSong: ChunithmMusicData?
44 | }
45 |
46 | struct UserChunithmRatingListEntry: Codable, Hashable, Equatable {
47 | let index: Int
48 | let musicId: Int
49 | let score: Int
50 | let levelIndex: Int
51 |
52 | var associatedBestEntry: UserChunithmBestScoreEntry?
53 | }
54 |
55 | struct UserChunithmRatingList: Codable, Hashable, Equatable {
56 | let best: [UserChunithmRatingListEntry]
57 | let recent: [UserChunithmRatingListEntry]
58 | let candidate: [UserChunithmRatingListEntry]
59 |
60 | static let empty = UserChunithmRatingList(best: [], recent: [], candidate: [])
61 | }
62 |
63 | struct UserChunithmPlayerInfo: Codable, Hashable, Equatable {
64 | let timestamp: Int
65 | var nickname: String
66 | let level: String
67 | let trophy: String
68 | let plate: String
69 | let dan: Int
70 | let ribbon: Int
71 | let rating: Double
72 | let maxRating: Double
73 | let rawOverpower: Double
74 | let percentOverpower: Double
75 | let lastPlayedDate: Int
76 | let friendCode: String
77 | let currentGold: Int
78 | let totalGold: Int
79 | let playCount: Int
80 | let charName: String
81 | let charUrl: String
82 | let charRank: String
83 | let charExp: Double
84 | let charIllust: String
85 | let ghostStatue: Int
86 | let silverStatue: Int
87 | let goldStatue: Int
88 | let rainbowStatue: Int
89 | }
90 |
91 | typealias UserChunithmBestScores = [UserChunithmBestScoreEntry]
92 | typealias UserChunithmRecentScores = [UserChunithmRecentScoreEntry]
93 | typealias UserChunithmPlayerInfos = [UserChunithmPlayerInfo]
94 |
--------------------------------------------------------------------------------
/chafenqi/Struct/Server/Chunithm/UserChunithmExtraEntry.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UserChunithmExtraEntry.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2024/12/24.
6 | //
7 |
8 | import Foundation
9 |
10 | struct UserChunithmExtra: Codable, Equatable {
11 | let characters: [UserChunithmCharacterEntry]
12 | let mapIcons: [UserChunithmMapIconEntry]
13 | let nameplates: [UserChunithmNameplateEntry]
14 | let skills: [UserChunithmSkillEntry]
15 | let tickets: [UserChunithmTicketEntry]
16 | let trophies: [UserChunithmTrophyEntry]
17 |
18 | static let empty = UserChunithmExtra(characters: [], mapIcons: [], nameplates: [], skills: [], tickets: [], trophies: [])
19 | }
20 |
21 | struct UserChunithmCharacterEntry: Codable, Equatable {
22 | let name: String
23 | let url: String
24 | let rank: String
25 | let exp: Double
26 | let current: Bool
27 | }
28 |
29 | struct UserChunithmMapIconEntry: Codable, Equatable {
30 | let name: String
31 | let url: String
32 | let current: Bool
33 | }
34 |
35 | struct UserChunithmNameplateEntry: Codable, Equatable {
36 | let name: String
37 | let url: String
38 | let current: Bool
39 | }
40 |
41 | struct UserChunithmSkillEntry: Codable, Equatable {
42 | let name: String
43 | let url: String
44 | let level: Int
45 | let description: String
46 | let current: Bool
47 | }
48 |
49 | struct UserChunithmTicketEntry: Codable, Equatable {
50 | let name: String
51 | let url: String
52 | let count: Int
53 | let description: String
54 | }
55 |
56 | struct UserChunithmTrophyEntry: Codable, Equatable {
57 | let name: String
58 | let type: String
59 | let description: String
60 | }
61 |
--------------------------------------------------------------------------------
/chafenqi/Struct/Server/Maimai/UserMaimaiEntry.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UserMaimaiBestScoreEntry.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2024/12/05.
6 | //
7 |
8 | import Foundation
9 |
10 | struct UserMaimaiBestScoreEntry: Codable, Hashable, Equatable {
11 | let musicId: Int
12 | let levelIndex: Int
13 | let type: String
14 | let achievements: Double
15 | let dxScore: Int
16 | let judgeStatus: String
17 | let syncStatus: String
18 | let lastModified: Int
19 |
20 | var associatedSong: MaimaiSongData?
21 | }
22 |
23 | struct UserMaimaiRecentScoreEntry: Codable, Hashable, Equatable {
24 | let timestamp: Int
25 | let musicId: Int
26 | let difficulty: String
27 | let type: String
28 | let achievements: Double
29 | let newRecord: Bool
30 | let dxScore: Int
31 | let judgeStatus: String
32 | let syncStatus: String
33 | let noteTap: Array
34 | let noteHold: Array
35 | let noteSlide: Array
36 | let noteTouch: Array
37 | let noteBreak: Array
38 | let maxCombo: String
39 | let maxSync: String
40 | let players: Array
41 |
42 | var associatedSong: MaimaiSongData?
43 | }
44 |
45 | struct UserMaimaiPlayerInfoEntry: Codable {
46 | let timestamp: Int
47 | var nickname: String
48 | let trophy: String
49 | let rating: Int
50 | let maxRating: Int
51 | let stars: Int
52 | let charUrl: String
53 | let gradeUrl: String
54 | let playCount: Int
55 | let stats: String
56 | }
57 |
58 | typealias UserMaimaiBestScores = [UserMaimaiBestScoreEntry]
59 | typealias UserMaimaiRecentScores = [UserMaimaiRecentScoreEntry]
60 | typealias UserMaimaiPlayerInfos = [UserMaimaiPlayerInfoEntry]
61 |
--------------------------------------------------------------------------------
/chafenqi/Struct/Server/Maimai/UserMaimaiExtraEntry.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MaimaiPlayerInfoDTO.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2024/12/05.
6 | //
7 |
8 | struct UserMaimaiExtra: Codable {
9 | let avatars: [UserMaimaiAvatarEntry]
10 | let characters: [UserMaimaiCharacterEntry]
11 | let frames: [UserMaimaiFrameEntry]
12 | let nameplates: [UserMaimaiNameplateEntry]
13 | let partners: [UserMaimaiPartnerEntry]
14 | let trophies: [UserMaimaiTrophyEntry]
15 |
16 | static let empty = UserMaimaiExtra(avatars: [], characters: [], frames: [], nameplates: [], partners: [], trophies: [])
17 | }
18 |
19 | struct UserMaimaiAvatarEntry: Codable, Equatable {
20 | let name: String
21 | let url: String
22 | let description: String
23 | let area: String
24 | let current: Bool
25 | }
26 |
27 | struct UserMaimaiCharacterEntry: Codable, Equatable {
28 | let name: String
29 | let url: String
30 | let description: String
31 | let level: String
32 | let area: String
33 | let current: Bool
34 | }
35 |
36 | struct UserMaimaiFrameEntry: Codable, Equatable {
37 | let name: String
38 | let url: String
39 | let description: String
40 | let area: String
41 | let current: Bool
42 | }
43 |
44 | struct UserMaimaiNameplateEntry: Codable, Equatable {
45 | let name: String
46 | let url: String
47 | let description: String
48 | let area: String
49 | let current: Bool
50 | }
51 |
52 | struct UserMaimaiPartnerEntry: Codable, Equatable {
53 | let name: String
54 | let url: String
55 | let description: String
56 | let current: Bool
57 | }
58 |
59 | struct UserMaimaiTrophyEntry: Codable, Equatable {
60 | let name: String
61 | let description: String
62 | let type: String
63 | let current: Bool
64 | }
65 |
--------------------------------------------------------------------------------
/chafenqi/Struct/Server/Team/TeamActivity.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TeamActivity.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2024/12/24.
6 | //
7 |
8 | import Foundation
9 |
10 | struct TeamActivity: Codable {
11 | let id: Int
12 | let timestamp: Int
13 | let userId: Int
14 | let activity: String
15 | }
16 |
--------------------------------------------------------------------------------
/chafenqi/Struct/Server/Team/TeamBasicInfo.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TeamBasicInfo.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2024/12/24.
6 | //
7 |
8 | import Foundation
9 |
10 | struct TeamBasicInfo: Codable {
11 | let id: Int
12 | let displayName: String
13 | let nameLastModifiedAt: Int
14 | let teamCode: String
15 | let leaderUserId: Int
16 | let style: String
17 | let remarks: String
18 | let promotable: Bool
19 | let lastMonthActivityPoints: Int
20 | let currentActivityPoints: Int
21 | let courseName: String
22 | let courseTrack1: String
23 | let courseTrack2: String
24 | let courseTrack3: String
25 | let courseHealth: Int
26 | let coursePrimaryErrorPenalty: Int
27 | let courseSecondaryErrorPenalty: Int
28 | let courseTertiaryErrorPenalty: Int
29 | let courseLastModifiedAt: Int
30 | let pinnedMessageId: Int?
31 | let createdAt: Int
32 | let lastActivityAt: Int
33 |
34 | static let empty = TeamBasicInfo(
35 | id: 0,
36 | displayName: "",
37 | nameLastModifiedAt: 0,
38 | teamCode: "",
39 | leaderUserId: 0,
40 | style: "",
41 | remarks: "",
42 | promotable: false,
43 | lastMonthActivityPoints: 0,
44 | currentActivityPoints: 0,
45 | courseName: "",
46 | courseTrack1: "",
47 | courseTrack2: "",
48 | courseTrack3: "",
49 | courseHealth: 0,
50 | coursePrimaryErrorPenalty: 0,
51 | courseSecondaryErrorPenalty: 0,
52 | courseTertiaryErrorPenalty: 0,
53 | courseLastModifiedAt: 0,
54 | pinnedMessageId: nil,
55 | createdAt: 0,
56 | lastActivityAt: 0
57 | )
58 | }
59 |
60 | struct TeamCourseTrack {
61 | let musicId: Int
62 | let levelIndex: Int
63 |
64 | init(musicId: Int, levelIndex: Int) {
65 | self.musicId = musicId
66 | self.levelIndex = levelIndex
67 | }
68 |
69 | init(trackString: String) {
70 | self.musicId = Int(trackString.split(separator: ",")[0]) ?? 0
71 | self.levelIndex = Int(trackString.split(separator: ",")[1]) ?? 0
72 | }
73 | }
74 |
75 | extension TeamBasicInfo {
76 | func courseTracks() -> [TeamCourseTrack]? {
77 | guard courseTrack1 != "" else {
78 | return nil
79 | }
80 |
81 | let track1 = TeamCourseTrack(trackString: courseTrack1)
82 | let track2 = TeamCourseTrack(trackString: courseTrack2)
83 | let track3 = TeamCourseTrack(trackString: courseTrack3)
84 |
85 | return [track1, track2, track3]
86 | }
87 |
88 | func activeDays() -> Int {
89 | let now = Date().timeIntervalSince1970
90 | let interval = now - Double(createdAt)
91 | return Int(interval / 86400)
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/chafenqi/Struct/Server/Team/TeamBulletinBoardEntry.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TeamBulletinBoardEntry.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2024/12/24.
6 | //
7 |
8 | import Foundation
9 |
10 | struct TeamBulletinBoardEntry: Codable {
11 | let id: Int
12 | let timestamp: Int
13 | let userId: Int
14 | let content: String
15 | }
16 |
--------------------------------------------------------------------------------
/chafenqi/Struct/Server/Team/TeamCourseRecord.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TeamCourseRecord.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2024/12/24.
6 | //
7 |
8 | import Foundation
9 |
10 | struct TeamCourseRecord: Codable {
11 | let id: Int
12 | let timestamp: Int
13 | let userId: Int
14 | let trackRecords: [TrackRecord]
15 | let cleared: Bool
16 |
17 | struct TrackRecord: Codable {
18 | let score: String
19 | let damage: Int
20 | }
21 | }
22 |
23 | extension TeamCourseRecord {
24 | func totalScore(mode: Int) -> String {
25 | return if mode == 0 {
26 | String(trackRecords.reduce(0) { $0 + (Int($1.score) ?? 0) })
27 | } else {
28 | String(format: "%.4f", trackRecords.reduce(0) { $0 + (Double($1.score.replacingOccurrences(of: "%", with: "")) ?? 0.0) }) + "%"
29 | }
30 | }
31 |
32 | func rawScore() -> Double {
33 | return trackRecords.reduce(0) { $0 + (Double($1.score.replacingOccurrences(of: "%", with: "")) ?? 0) }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/chafenqi/Struct/Server/Team/TeamCreatePayload.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TeamCreatePayload.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2024/12/24.
6 | //
7 |
8 | import Foundation
9 |
10 | struct TeamCreatePayload: Codable {
11 | let game: Int
12 | let displayName: String
13 | let style: String
14 | let remarks: String
15 | let promotable: Bool
16 | }
17 |
--------------------------------------------------------------------------------
/chafenqi/Struct/Server/Team/TeamInfo.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TeamInfo.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2024/12/24.
6 | //
7 |
8 | import Foundation
9 |
10 | struct TeamInfo: Codable {
11 | let info: TeamBasicInfo
12 | let members: [TeamMember]
13 | let pendingMembers: [TeamPendingMember]
14 | let activities: [TeamActivity]
15 | let bulletinBoard: [TeamBulletinBoardEntry]
16 | let courseRecords: [TeamCourseRecord]
17 |
18 | static let empty = TeamInfo(
19 | info: TeamBasicInfo.empty,
20 | members: [],
21 | pendingMembers: [],
22 | activities: [],
23 | bulletinBoard: [],
24 | courseRecords: []
25 | )
26 | }
27 |
--------------------------------------------------------------------------------
/chafenqi/Struct/Server/Team/TeamMember.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TeamMember.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2024/12/24.
6 | //
7 |
8 | import Foundation
9 |
10 | struct TeamMember: Codable {
11 | let id: Int
12 | let userId: Int
13 | let nickname: String
14 | let avatar: String
15 | let trophy: String
16 | let rating: String
17 | let joinAt: Int
18 | let activityPoints: Int
19 | let playCount: Int
20 | let lastActivityAt: Int
21 | }
22 |
--------------------------------------------------------------------------------
/chafenqi/Struct/Server/Team/TeamPendingMember.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TeamPendingMember.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2024/12/24.
6 | //
7 |
8 | import Foundation
9 |
10 | struct TeamPendingMember: Codable {
11 | let id: Int
12 | let userId: Int
13 | let nickname: String
14 | let avatar: String
15 | let trophy: String
16 | let rating: String
17 | let timestamp: Int
18 | let status: String
19 | let message: String
20 | }
21 |
--------------------------------------------------------------------------------
/chafenqi/Struct/Server/Team/TeamUpdateCoursePayload.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TeamUpdateCoursePayload.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2024/12/24.
6 | //
7 |
8 | import Foundation
9 |
10 | struct TeamUpdateCoursePayload: Codable {
11 | let courseName: String
12 | let courseTrack1: CourseEntry
13 | let courseTrack2: CourseEntry
14 | let courseTrack3: CourseEntry
15 | let courseHealth: Int
16 |
17 | struct CourseEntry: Codable {
18 | let musicId: Int
19 | let levelIndex: Int
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/chafenqi/Struct/Widget/WidgetData.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WidgetData.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2023/6/30.
6 | //
7 |
8 | import Foundation
9 |
10 | struct WidgetData {
11 | struct Customization: Codable, Equatable {
12 | enum Module: Codable {
13 | case playCount, rating, lastUpload
14 | }
15 |
16 | var maiCharType: Int?
17 | var maiCharUrl: String?
18 | var maiBgUrl: String?
19 | var maiColor: [[CGFloat]]?
20 | var maiBgBlur: Double?
21 | var chuCharUrl: String?
22 | var chuBgUrl: String?
23 | var chuColor: [[CGFloat]]?
24 | var chuBgBlur: Double?
25 |
26 | var smallModuleList: [Module]?
27 | var mediumModuleList: [Module]?
28 | var bigModuleList: [Module]?
29 |
30 | // chu big, chu small, mai big, mai small
31 | var darkModes: [Bool] = [false, false, false, false]
32 | }
33 |
34 | var username: String
35 | var isPremium: Bool
36 |
37 | var maimaiInfo: UserMaimaiPlayerInfoEntry?
38 | var chunithmInfo: UserChunithmPlayerInfo?
39 |
40 | var maiRecentOne: UserMaimaiRecentScoreEntry?
41 | var chuRecentOne: UserChunithmRecentScoreEntry?
42 |
43 | var chuChar: Data?
44 | var chuBg: Data?
45 | var chuCover: Data?
46 |
47 | var maiChar: Data?
48 | var maiBg: Data?
49 | var maiCover: Data?
50 |
51 | var custom: Customization?
52 | }
53 |
--------------------------------------------------------------------------------
/chafenqi/View/v1/ScoreView/B30View.swift:
--------------------------------------------------------------------------------
1 | //
2 | // B30View.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2023/1/26.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct B30View: View {
11 | @AppStorage("loadedChunithmSongs") var loadedSongs: Data = Data()
12 |
13 | var decodedLoadedSongs: Array
14 |
15 | @State private var showingMaximumRating = false
16 |
17 | var b30 = ArraySlice()
18 |
19 | let rows = [
20 | GridItem(),
21 | GridItem()
22 | ]
23 |
24 | var body: some View {
25 | NavigationView {
26 | VStack {
27 | HStack {
28 | ZStack {
29 | // TODO: get max
30 | CutCircularProgressView(progress: showingMaximumRating ? 1 : userInfo.getAvgB30() / 17.30, lineWidth: 14, width: 100, color: Color.cyan)
31 |
32 | Text("\(userInfo.getAvgB30(), specifier: "%.2f")")
33 | .foregroundColor(Color.cyan)
34 | .textFieldStyle(.roundedBorder)
35 | .font(.title)
36 |
37 | Text("B30")
38 | .padding(.top, 70)
39 | .font(.title2)
40 | }
41 | .padding()
42 |
43 | VStack {
44 | Spacer()
45 | Text("总游玩曲目:\(userInfo.records.best.count)")
46 | Text("a")
47 | }
48 | }
49 | }.task {
50 |
51 | }
52 | }
53 | }
54 | }
55 |
56 | struct B30View_Previews: PreviewProvider {
57 | static var previews: some View {
58 | MainView(currentTab: .constant(.home))
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/chafenqi/View/v1/SongView/ChunithmBasicView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SongBasicInfoView.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2023/1/8.
6 | //
7 |
8 | import SwiftUI
9 | import CachedAsyncImage
10 |
11 | // https://gitee.com/louiswu2011/chunithm-cover/raw/master/image/99.png
12 |
13 |
14 |
15 | struct ChunithmBasicView: View {
16 |
17 | let song: ChunithmSongData
18 |
19 | @Environment(\.colorScheme) var colorScheme
20 |
21 | @AppStorage("settingsChunithmCoverSource") var coverSource = 0
22 |
23 | @State private var showingChartConstant = false
24 |
25 | var body: some View {
26 | HStack() {
27 | let requestURL = coverSource == 0 ? URL(string: "https://raw.githubusercontent.com/Louiswu2011/Chunithm-Song-Cover/main/images/\(song.musicId).png") : URL(string: "https://gitee.com/louiswu2011/chunithm-cover/raw/master/image/\(song.musicId).png")
28 |
29 | SongCoverView(coverURL: requestURL!, size: 80, cornerRadius: 10, withShadow: false)
30 | .overlay {
31 | RoundedRectangle(cornerRadius: 10)
32 | .stroke(colorScheme == .dark ? .white.opacity(0.33) : .black.opacity(0.33), lineWidth: 1)
33 | }
34 |
35 | HStack{
36 | VStack(alignment: .leading) {
37 | Text(song.title)
38 | .font(.system(size: 20))
39 | .bold()
40 | .frame(maxWidth: .infinity, alignment: .leading)
41 | .lineLimit(1)
42 | .textSelection(.enabled)
43 |
44 | Text(song.basicInfo.artist)
45 | .font(.system(size: 15))
46 | .lineLimit(1)
47 | .textSelection(.enabled)
48 |
49 | Spacer()
50 |
51 |
52 | LevelStripView(mode: 0, levels: song.level)
53 |
54 |
55 | }
56 | }
57 | }
58 | }
59 | }
60 |
61 |
62 |
63 | struct SongBasicInfoView_Previews: PreviewProvider {
64 | static var previews: some View {
65 | ChunithmListView()
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/chafenqi/View/v1/SongView/SongBarView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SongBarView.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2023/2/14.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct SongBarView: View {
11 | let song: ScoreEntry
12 |
13 | var body: some View {
14 | ZStack {
15 | RoundedRectangle(cornerRadius: 5)
16 | .foregroundColor(chunithmLevelColor[song.levelIndex]!)
17 |
18 | HStack(spacing: 0) {
19 | Text(song.title)
20 | .if(song.levelIndex == 4) { view in
21 | view.foregroundColor(.white)
22 | }
23 | .padding(.leading)
24 |
25 | Spacer()
26 |
27 |
28 | if (song.getStatus() != "Clear") {
29 | ZStack {
30 | Rectangle()
31 | .foregroundColor(song.getClearBadgeColor())
32 |
33 | Text(song.getStatus())
34 | .if(song.levelIndex == 4) { view in
35 | view.foregroundColor(.white)
36 | }
37 | }
38 | .frame(width: 40)
39 |
40 | }
41 |
42 | ZStack {
43 | Rectangle()
44 | .foregroundColor(.teal)
45 |
46 | Text(String(song.score))
47 | .bold()
48 | .if(song.levelIndex == 4) { view in
49 | view.foregroundColor(.white)
50 | }
51 | }
52 | .frame(width: 90)
53 | .mask {
54 | RoundedRectangle(cornerRadius: 5)
55 | .frame(width: 100)
56 | .padding(.trailing, 10)
57 | }
58 | }
59 | }
60 | .frame(height: 30)
61 | .padding(.horizontal)
62 | }
63 | }
64 |
65 | struct SongBarView_Previews: PreviewProvider {
66 | static var previews: some View {
67 | SongBarView(song: song)
68 | }
69 | }
70 |
71 | extension View {
72 | /// Applies the given transform if the given condition evaluates to `true`.
73 | /// - Parameters:
74 | /// - condition: The condition to evaluate.
75 | /// - transform: The transform to apply to the source `View`.
76 | /// - Returns: Either the original `View` or the modified `View` if the condition is `true`.
77 | @ViewBuilder func `if`(_ condition: Bool, transform: (Self) -> Content) -> some View {
78 | if condition {
79 | transform(self)
80 | } else {
81 | self
82 | }
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/chafenqi/View/v1/TopView/HomeView/Module/RatingAnalysisView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RatingAnalysisView.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2023/3/22.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct RatingAnalysisView: View {
11 | var body: some View {
12 | VStack {
13 | HStack {
14 | Text("B30")
15 | .bold()
16 | Text("平均")
17 | }
18 | }
19 | }
20 | }
21 |
22 | struct RatingAnalysisView_Previews: PreviewProvider {
23 | static var previews: some View {
24 | RatingAnalysisView()
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/chafenqi/View/v1/TopView/LoginView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LoginView.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2023/3/22.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct LoginView: View {
11 | @State var account: String = ""
12 | @State var password: String = ""
13 |
14 | var body: some View {
15 | VStack {
16 | Text("登录到查分器")
17 | .font(.title)
18 | .bold()
19 | .padding(.bottom, 20)
20 | VStack(spacing: 15) {
21 | HStack {
22 | TextField("用户名", text: $account)
23 | .autocorrectionDisabled(true)
24 | .autocapitalization(.none)
25 | }
26 | HStack {
27 | SecureField("密码", text: $password)
28 | }
29 | }
30 | .padding(.horizontal, 30)
31 | .padding(.bottom, 20)
32 | Button {
33 |
34 | } label: {
35 | Text("登录")
36 | }
37 | }
38 | .padding()
39 | }
40 | }
41 |
42 | struct LoginView_Previews: PreviewProvider {
43 | static var previews: some View {
44 | LoginView()
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/chafenqi/View/v2/Comment/CommentCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CommentCell.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2023/3/13.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct CommentCell: View {
11 | @State var comment: UserComment
12 |
13 | var body: some View {
14 | VStack(alignment: .leading) {
15 | HStack {
16 | Text(comment.username)
17 | .font(.system(size: 15))
18 | .bold()
19 | .lineLimit(1)
20 | Spacer()
21 | Text(comment.dateString)
22 | .font(.system(size: 15))
23 | .foregroundColor(.gray)
24 | }
25 | .padding(.horizontal)
26 | .padding(.top)
27 |
28 | Text(comment.content)
29 | .multilineTextAlignment(.leading)
30 | .lineLimit(2)
31 | .padding(.horizontal, 20)
32 | .padding(.vertical)
33 | }
34 |
35 | }
36 | }
37 |
38 | struct CommentCell_Previews: PreviewProvider {
39 | static var previews: some View {
40 | CommentCell(comment: UserComment.shared)
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/chafenqi/View/v2/Comment/CommentComposer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CommentComposer.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2023/3/17.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct CommentComposerView: View {
11 | @ObservedObject var toastManager = AlertToastManager.shared
12 | @ObservedObject var user: CFQNUser
13 |
14 | var musicId = 0
15 | var musicFrom = 0
16 | @State var message = ""
17 | // @State var replyComment: Comment? = nil
18 |
19 | @Binding var showingComposer: Bool
20 |
21 | var body: some View {
22 | NavigationView {
23 | VStack(alignment: .leading) {
24 | TextField("在这里输入你的评论...", text: $message)
25 | .autocorrectionDisabled(true)
26 | .multilineTextAlignment(.leading)
27 | .autocapitalization(.none)
28 | Spacer()
29 | Text("将以\(user.username)的身份发布,请文明发言")
30 | .font(.system(size: 15))
31 | .foregroundColor(.gray)
32 | }
33 | .padding()
34 | .navigationBarTitle("发表评论")
35 | .navigationBarTitleDisplayMode(.inline)
36 | .toolbar {
37 | ToolbarItem(placement: .navigationBarTrailing) {
38 | Button {
39 | Task {
40 | do {
41 | let result = try await CFQCommentServer.postComment(authToken: user.jwtToken, content: message, mode: musicFrom, musicId: musicId)
42 | if (result) {
43 | showingComposer.toggle()
44 | } else {
45 | print("[CommentComposer] Failed to post comment.")
46 | }
47 | } catch {
48 | // TODO: Error handling
49 | print(error)
50 | }
51 | }
52 | } label: {
53 | Text("提交")
54 | }
55 | }
56 |
57 | ToolbarItem(placement: .navigationBarLeading) {
58 | Button {
59 | showingComposer.toggle()
60 | } label: {
61 | Text("取消")
62 | }
63 | }
64 | }
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/chafenqi/View/v2/Custom/CustomContextPreview.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CustomContextPreview.swift
3 | // chafenqi
4 | //
5 | // Created by xinyue on 2023/7/13.
6 | //
7 |
8 | import Foundation
9 | import SwiftUI
10 |
11 | struct PreviewContextMenu {
12 | let destination: Content
13 | let actionProvider: UIContextMenuActionProvider?
14 |
15 | init(destination: Content, actionProvider: UIContextMenuActionProvider? = nil) {
16 | self.destination = destination
17 | self.actionProvider = actionProvider
18 | }
19 | }
20 |
21 | // UIView wrapper with UIContextMenuInteraction
22 | struct PreviewContextView: UIViewRepresentable {
23 |
24 | let menu: PreviewContextMenu
25 | let didCommitView: () -> Void
26 |
27 | func makeUIView(context: Context) -> UIView {
28 | let view = UIView()
29 | view.backgroundColor = .clear
30 | let menuInteraction = UIContextMenuInteraction(delegate: context.coordinator)
31 | view.addInteraction(menuInteraction)
32 | return view
33 | }
34 |
35 | func updateUIView(_ uiView: UIView, context: Context) { }
36 |
37 | func makeCoordinator() -> Coordinator {
38 | return Coordinator(menu: self.menu, didCommitView: self.didCommitView)
39 | }
40 |
41 | class Coordinator: NSObject, UIContextMenuInteractionDelegate {
42 |
43 | let menu: PreviewContextMenu
44 | let didCommitView: () -> Void
45 |
46 | init(menu: PreviewContextMenu, didCommitView: @escaping () -> Void) {
47 | self.menu = menu
48 | self.didCommitView = didCommitView
49 | }
50 |
51 | func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
52 | return UIContextMenuConfiguration(identifier: nil, previewProvider: { () -> UIViewController? in
53 | UIHostingController(rootView: self.menu.destination)
54 | }, actionProvider: self.menu.actionProvider)
55 | }
56 |
57 | func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
58 | animator.addCompletion(self.didCommitView)
59 | }
60 |
61 | }
62 | }
63 |
64 | // Add context menu modifier
65 | extension View {
66 | func contextMenu(_ menu: PreviewContextMenu) -> some View {
67 | self.modifier(PreviewContextViewModifier(menu: menu))
68 | }
69 | }
70 |
71 | struct PreviewContextViewModifier: ViewModifier {
72 |
73 | let menu: PreviewContextMenu
74 | @Environment(\.presentationMode) var mode
75 |
76 | @State var isActive: Bool = false
77 |
78 | func body(content: Content) -> some View {
79 | Group {
80 | if isActive {
81 | menu.destination
82 | } else {
83 | content.overlay(PreviewContextView(menu: menu, didCommitView: { self.isActive = true }))
84 | }
85 | }
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/chafenqi/View/v2/Custom/GradeBadgeView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GradeBadgeView.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2023/3/22.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct GradeBadgeView: View {
11 | @State var grade: String = "SSS+"
12 |
13 | var body: some View {
14 | ZStack {
15 | RoundedRectangle(cornerRadius: 5)
16 | .foregroundColor(backgroundColor)
17 |
18 | if (grade == "SSS+") {
19 | RoundedRectangle(cornerRadius: 5)
20 | .stroke(AngularGradient(gradient: Gradient(colors: [.red, .yellow, .green, .blue, .purple, .red]), center: .center), lineWidth: 2)
21 | .foregroundColor(.clear)
22 | }
23 |
24 | HStack(spacing: 0) {
25 | Group {
26 | Text(grade.replacingOccurrences(of: "+", with: ""))
27 | .bold()
28 | if (grade.contains("+")) {
29 | Text("+")
30 | .bold()
31 | .font(.system(size: 15))
32 | .padding(.bottom, 6)
33 | }
34 | }
35 | .foregroundColor(foregroundColor)
36 | }
37 | }
38 | .frame(width: 55, height: 24)
39 | }
40 |
41 | var backgroundColor: Color {
42 | switch (grade) {
43 | case "SSS+", "SSS", "SS+", "SS", "S+", "S":
44 | return .yellow
45 | case "AAA":
46 | return Color(red: 191, green: 155, blue: 48)
47 | default:
48 | return .gray
49 | }
50 | }
51 |
52 | var foregroundColor: Color {
53 | switch (grade) {
54 | case "SSS+":
55 | return .black
56 | case "SSS", "SS+", "SS", "S+", "S":
57 | return .black
58 | case "AAA":
59 | return .black
60 | default:
61 | return .white
62 | }
63 | }
64 |
65 | }
66 |
67 | struct GradeBadgeView_Previews: PreviewProvider {
68 | static var previews: some View {
69 | GradeBadgeView()
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/chafenqi/View/v2/Custom/LevelBlockView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LevelBlockView.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2023/2/5.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct LevelStripView: View {
11 | var mode: Int
12 | var levels: Array
13 |
14 | var body: some View {
15 | let levelColor = mode == 0 ? chunithmLevelColor : maimaiLevelColor
16 |
17 | HStack {
18 | ForEach(levels.indices, id: \.self) { index in
19 | if let color = levelColor[index], levels[index] != "0" && !levels[index].isEmpty {
20 | LevelBlockView(color: color, level: levels[index])
21 | }
22 | }
23 | }
24 | }
25 | }
26 |
27 | struct LevelBlockView: View {
28 | var color: Color
29 | var level: String
30 |
31 | var body: some View {
32 | ZStack {
33 | RoundedRectangle(cornerRadius: 4)
34 | .foregroundColor(color)
35 |
36 | Text(level)
37 | .font(.system(size: 15))
38 | .foregroundColor(.white)
39 | }
40 | .frame(width: 30, height: 20)
41 | }
42 | }
43 |
44 | struct LevelBlockView_Previews: PreviewProvider {
45 | static var previews: some View {
46 | LevelStripView(mode: 0, levels: ["7", "9", "11", "13+", "14+"])
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/chafenqi/View/v2/Custom/TextInfoView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TextInfoView.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2023/1/31.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct TextInfoView: View {
11 | @State var text: String
12 | @State var info: String
13 |
14 | var body: some View {
15 | HStack {
16 | Text(text)
17 | Spacer()
18 | Text(info)
19 | .foregroundColor(Color.gray)
20 | .lineLimit(1)
21 | }
22 | }
23 | }
24 |
25 |
--------------------------------------------------------------------------------
/chafenqi/View/v2/Delta/Charts/PCDeltaChart.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PCDeltaChart.swift
3 | // chafenqi
4 | //
5 | // Created by xinyue on 2023/5/29.
6 | //
7 |
8 | import SwiftUI
9 | import SwiftUICharts
10 |
11 | struct PCDeltaChart: View {
12 | @Binding var rawDataPoints: [(Double, String)]
13 | @Binding var shouldShowMarkers: Bool
14 |
15 | var body: some View {
16 | let chartData = addDataPoints()
17 | VStack {
18 | if shouldShowMarkers {
19 | LineChart(chartData: chartData)
20 | .pointMarkers(chartData: chartData)
21 | .touchOverlay(chartData: chartData)
22 | .yAxisGrid(chartData: chartData)
23 | .yAxisLabels(chartData: chartData)
24 | .floatingInfoBox(chartData: chartData)
25 | .headerBox(chartData: chartData)
26 | .transaction { transaction in
27 | transaction.animation = nil
28 | }
29 | .id(UUID())
30 | } else {
31 | LineChart(chartData: chartData)
32 | .touchOverlay(chartData: chartData)
33 | .yAxisGrid(chartData: chartData)
34 | .yAxisLabels(chartData: chartData)
35 | .floatingInfoBox(chartData: chartData)
36 | .headerBox(chartData: chartData)
37 | .transaction { transaction in
38 | transaction.animation = nil
39 | }
40 | .id(UUID())
41 | }
42 | }
43 | }
44 |
45 | func addDataPoints() -> LineChartData {
46 | var dataPoints = [LineChartDataPoint]()
47 | for (point, description) in rawDataPoints {
48 | dataPoints.append(LineChartDataPoint(value: point, xAxisLabel: description, description: description))
49 | }
50 | let data = LineDataSet(
51 | dataPoints: dataPoints,
52 | legendTitle: "次数",
53 | pointStyle: .init(),
54 | style: .init(lineColour: .init(colour: .blue), lineType: .curvedLine)
55 | )
56 | let metadata = ChartMetadata(title: "游玩次数", subtitle: "全部数据")
57 | let chartStyle = LineChartStyle(
58 | infoBoxPlacement: .floating,
59 | infoBoxBorderColour: Color.primary,
60 | infoBoxBorderStyle: StrokeStyle(lineWidth: 1),
61 | markerType: .indicator(style: .init()),
62 | baseline: .minimumValue,
63 | topLine: .maximumValue
64 | )
65 | return LineChartData(dataSets: data, metadata: metadata, chartStyle: chartStyle)
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/chafenqi/View/v2/Delta/Charts/RatingDeltaChart.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RatingDeltaCharat.swift
3 | // chafenqi
4 | //
5 | // Created by xinyue on 2023/5/29.
6 | //
7 |
8 | import SwiftUI
9 | import SwiftUICharts
10 |
11 | struct RatingDeltaChart: View {
12 | @Binding var rawDataPoints: [(Double, String)]
13 | @State var isChunithm = true
14 | @Binding var shouldShowMarkers: Bool
15 |
16 | var body: some View {
17 | let chartData = addDataPoints()
18 | VStack {
19 | if shouldShowMarkers {
20 | LineChart(chartData: chartData)
21 | .pointMarkers(chartData: chartData)
22 | .touchOverlay(chartData: chartData, specifier: isChunithm ? "%.2f" : "%.0f")
23 | .yAxisGrid(chartData: chartData)
24 | .yAxisLabels(chartData: chartData, specifier: isChunithm ? "%.2f" : "%.0f")
25 | .floatingInfoBox(chartData: chartData)
26 | .headerBox(chartData: chartData)
27 | .transaction { transaction in
28 | transaction.animation = nil
29 | }
30 | .id(UUID())
31 | } else {
32 | LineChart(chartData: chartData)
33 | .touchOverlay(chartData: chartData, specifier: isChunithm ? "%.2f" : "%.0f")
34 | .yAxisGrid(chartData: chartData)
35 | .yAxisLabels(chartData: chartData, specifier: isChunithm ? "%.2f" : "%.0f")
36 | .floatingInfoBox(chartData: chartData)
37 | .headerBox(chartData: chartData)
38 | .transaction { transaction in
39 | transaction.animation = nil
40 | }
41 | .id(UUID())
42 | }
43 | }
44 | }
45 |
46 | func addDataPoints() -> LineChartData {
47 | var dataPoints = [LineChartDataPoint]()
48 | for (point, description) in rawDataPoints {
49 | dataPoints.append(LineChartDataPoint(value: point, xAxisLabel: description, description: description))
50 | }
51 | let data = LineDataSet(
52 | dataPoints: dataPoints,
53 | legendTitle: "Rating",
54 | pointStyle: .init(),
55 | style: .init(lineColour: .init(colour: .red), lineType: .curvedLine)
56 | )
57 | let metadata = ChartMetadata(title: "Rating", subtitle: "全部数据")
58 | let chartStyle = LineChartStyle(
59 | infoBoxPlacement: .floating,
60 | infoBoxBorderColour: Color.primary,
61 | infoBoxBorderStyle: StrokeStyle(lineWidth: 1),
62 | markerType: .bottomLeading(attachment: .line(dot: .style(.init()))),
63 | baseline: .minimumValue,
64 | topLine: .maximumValue
65 | )
66 | return LineChartData(dataSets: data, metadata: metadata, chartStyle: chartStyle)
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/chafenqi/View/v2/Home/HomeDelta.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HomeDelta.swift
3 | // chafenqi
4 | //
5 | // Created by xinyue on 2023/6/1.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct HomeDelta: View {
11 | @ObservedObject var user = CFQNUser()
12 |
13 | var body: some View {
14 | VStack {
15 | HStack {
16 | Text("出勤记录")
17 | .font(.system(size: 20))
18 | .bold()
19 | Spacer()
20 |
21 | NavigationLink {
22 | if user.isPremium {
23 | DeltaListView(user: user)
24 | } else {
25 | NotPremiumView()
26 | }
27 | } label: {
28 | Text("显示全部")
29 | .font(.system(size: 18))
30 | }
31 | }
32 |
33 | if user.isPremium {
34 | if user.currentMode == 0 {
35 | DeltaShortLook(user: user)
36 | .padding(.top, 5)
37 | } else if user.currentMode == 1 {
38 | DeltaShortLook(user: user)
39 | .padding(.top, 5)
40 | }
41 | }
42 | }
43 | .padding()
44 | }
45 | }
46 |
47 | struct HomeDelta_Previews: PreviewProvider {
48 | static var previews: some View {
49 | HomeDelta()
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/chafenqi/View/v2/Home/HomeRating.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HomeRating.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2023/5/9.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct HomeRating: View {
11 | @ObservedObject var user: CFQNUser
12 |
13 | var body: some View {
14 | VStack {
15 | HStack {
16 | Text("Rating分析")
17 | .font(.system(size: 20))
18 | .bold()
19 | Spacer()
20 |
21 | NavigationLink {
22 | RatingListView(user: user)
23 | } label: {
24 | Text("显示全部")
25 | .font(.system(size: 18))
26 | }
27 | }
28 |
29 | if user.currentMode == 0 && user.chunithm.rating.best.count > 0 {
30 | RatingShortLook(user: user, length: min(30, user.chunithm.rating.best.count), coverUrl: ChunithmDataGrabber.getSongCoverUrl(source: 0, musicId: String(user.chunithm.rating.best.first?.musicId ?? 0)))
31 | .frame(width: UIScreen.main.bounds.size.width - 30)
32 | } else if user.currentMode == 1 && user.maimai.custom.pastSlice.count > 0 {
33 | RatingShortLook(user: user, length: min(50, user.maimai.custom.pastSlice.count + user.maimai.custom.currentSlice.count), coverUrl: MaimaiDataGrabber.getSongCoverUrl(source: 0, coverId: user.maimai.custom.pastSlice.first?.associatedSong?.musicId ?? 0))
34 | .frame(width: UIScreen.main.bounds.size.width - 30)
35 | }
36 | }
37 | .padding()
38 | }
39 | }
40 |
41 | struct HomeRating_Previews: PreviewProvider {
42 | static var previews: some View {
43 | HomeRating(user: CFQNUser())
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/chafenqi/View/v2/Home/HomeTeam.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HomeTeam.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2024/12/24.
6 | //
7 |
8 | import Foundation
9 | import SwiftUI
10 | import Inject
11 |
12 | struct HomeTeam: View {
13 | @ObserveInjection var inject
14 | @ObservedObject var team: CFQTeam
15 | @ObservedObject var user: CFQNUser
16 |
17 | var body: some View {
18 | HStack {
19 | NavigationLink {
20 | TeamLandingPage(team: team, user: user)
21 | } label: {
22 | ZStack {
23 | RoundedRectangle(cornerRadius: 5)
24 | .fill(user.currentMode == 0 ?
25 | user.homeUseCurrentVersionTheme ? nameplateThemedChuniGradientStyle : nameplateDefaultChuniGradientStyle :
26 | user.homeUseCurrentVersionTheme ? nameplateThemedMaiGradientStyle : nameplateDefaultMaiGradientStyle
27 | )
28 | if team.isLoading {
29 | ProgressView()
30 | } else {
31 | Label(team.current.info.displayName.isEmpty ? "加入或创建团队" : team.current.info.displayName, systemImage: "person.3.fill")
32 | .foregroundColor(.black)
33 | .padding(5)
34 | }
35 | }
36 | }
37 | NavigationLink {
38 | TeamLeaderboardPage(team: team, user: user)
39 | } label: {
40 | ZStack {
41 | RoundedRectangle(cornerRadius: 5)
42 | .fill(user.currentMode == 0 ?
43 | user.homeUseCurrentVersionTheme ? nameplateThemedChuniGradientStyle : nameplateDefaultChuniGradientStyle :
44 | user.homeUseCurrentVersionTheme ? nameplateThemedMaiGradientStyle : nameplateDefaultMaiGradientStyle
45 | )
46 | Label("团队排行榜", systemImage: "chart.bar.fill")
47 | .foregroundColor(.black)
48 | .padding(5)
49 | }
50 | }
51 | }
52 | .enableInjection()
53 | .padding(.horizontal)
54 | .lineLimit(1)
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/chafenqi/View/v2/Info/ChunithmList/InfoChunithmCharacterList.swift:
--------------------------------------------------------------------------------
1 | //
2 | // InfoCharacterList.swift
3 | // chafenqi
4 | //
5 | // Created by xinyue on 2023/6/7.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct InfoCharacterList: View {
11 | var characters: [UserChunithmCharacterEntry]
12 |
13 | var body: some View {
14 | Form {
15 | Section {
16 | ForEach(characters, id: \.name) { char in
17 | InfoCharacterItem(character: char)
18 | }
19 | } header: {
20 | Text("共\(characters.count)名角色")
21 | }
22 | }
23 | .navigationTitle("角色列表")
24 | .navigationBarTitleDisplayMode(.inline)
25 | }
26 | }
27 |
28 | struct InfoCharacterItem: View {
29 | @Environment(\.managedObjectContext) var context
30 | var character: UserChunithmCharacterEntry
31 |
32 | var body: some View {
33 | HStack {
34 | AsyncImage(url: URL(string: character.url)!, context: context, placeholder: {
35 | ProgressView()
36 | }, image: { img in
37 | Image(uiImage: img)
38 | .resizable()
39 | })
40 | .aspectRatio(1 ,contentMode: .fit)
41 | .mask(RoundedRectangle(cornerRadius: 5))
42 | .frame(width: 50)
43 |
44 | VStack {
45 | HStack {
46 | Text(character.name)
47 | .lineLimit(1)
48 | Spacer()
49 | if character.current {
50 | Text("当前角色")
51 | .bold()
52 | }
53 | Text("LV \(character.rank)")
54 | }
55 | Spacer()
56 | ProgressView(value: character.exp)
57 | }
58 | .padding(.vertical, 5)
59 | }
60 | .frame(height: 60)
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/chafenqi/View/v2/Info/ChunithmList/InfoChunithmMapIconList.swift:
--------------------------------------------------------------------------------
1 | //
2 | // InfoChunithmMapIconList.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2023/8/31.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct InfoChunithmMapIconList: View {
11 | var mapIcons: [UserChunithmMapIconEntry]
12 |
13 | var body: some View {
14 | Form {
15 | Section {
16 | ForEach(mapIcons, id: \.name) { mapIcon in
17 | InfoChunithmMapIconItem(mapIcon: mapIcon)
18 | }
19 | } header: {
20 | Text("共\(mapIcons.count)个地图头像")
21 | }
22 | }
23 | .navigationTitle("地图头像列表")
24 | .navigationBarTitleDisplayMode(.inline)
25 | }
26 | }
27 |
28 | struct InfoChunithmMapIconItem: View {
29 | @Environment(\.managedObjectContext) var context
30 | var mapIcon: UserChunithmMapIconEntry
31 |
32 | var body: some View {
33 | HStack {
34 | AsyncImage(url: URL(string: mapIcon.url)!, context: context, placeholder: {
35 | ProgressView()
36 | }, image: { img in
37 | Image(uiImage: img)
38 | .resizable()
39 | })
40 | .aspectRatio(contentMode: .fit)
41 | .frame(width: 50)
42 |
43 | Text(mapIcon.name)
44 | .lineLimit(1)
45 |
46 | Spacer()
47 |
48 | if mapIcon.current {
49 | Text("当前头像")
50 | .bold()
51 | }
52 | }
53 | .frame(height: 60)
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/chafenqi/View/v2/Info/ChunithmList/InfoChunithmNameplateList.swift:
--------------------------------------------------------------------------------
1 | //
2 | // InfoChunithmNameplateView.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2023/7/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct InfoChunithmNameplateList: View {
11 | @Environment(\.managedObjectContext) var context
12 | var list = [UserChunithmNameplateEntry]()
13 |
14 | @State private var image = [String: UIImage]()
15 |
16 | var body: some View {
17 | Form {
18 | Section {
19 | ForEach(list, id: \.url) { nameplate in
20 | HStack {
21 | Spacer()
22 | VStack(alignment: .center) {
23 | AsyncImage(url: URL(string: nameplate.url)!, context: context, placeholder: {
24 | ProgressView()
25 | }, image: { img in
26 | let _ = DispatchQueue.main.async {
27 | self.image[nameplate.url] = img
28 | }
29 | Image(uiImage: img)
30 | .resizable()
31 |
32 | })
33 | .aspectRatio(contentMode: .fit)
34 | .frame(height: 60)
35 | .contextMenu {
36 | Button {
37 | if let image = self.image[nameplate.url] {
38 | UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
39 | }
40 | } label: {
41 | Label("保存到相册", systemImage: "square.and.arrow.down")
42 | }
43 | }
44 | Text(nameplate.name)
45 | .bold()
46 | }
47 | Spacer()
48 | }
49 | }
50 | } header: {
51 | Text("共\(list.count)个名牌版")
52 | }
53 | }
54 | .navigationTitle("名牌版一览")
55 | .navigationBarTitleDisplayMode(.inline)
56 | }
57 | }
58 |
59 | struct InfoChunithmNameplateList_Previews: PreviewProvider {
60 | static var previews: some View {
61 | InfoChunithmNameplateList()
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/chafenqi/View/v2/Info/ChunithmList/InfoChunithmSkillList.swift:
--------------------------------------------------------------------------------
1 | //
2 | // InfoSkillList.swift
3 | // chafenqi
4 | //
5 | // Created by xinyue on 2023/6/7.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct InfoSkillList: View {
11 | var skills: [UserChunithmSkillEntry]
12 |
13 | var body: some View {
14 | Form {
15 | ForEach(skills, id: \.name) { skill in
16 | InfoSkillItem(skill: skill)
17 | }
18 | }
19 | .navigationTitle("技能列表")
20 | .navigationBarTitleDisplayMode(.inline)
21 | }
22 | }
23 |
24 | struct InfoSkillItem: View {
25 | @Environment(\.managedObjectContext) var context
26 | var skill: UserChunithmSkillEntry
27 |
28 | var body: some View {
29 | HStack {
30 | AsyncImage(url: URL(string: skill.url)!, context: context, placeholder: {
31 | ProgressView()
32 | }, image: { img in
33 | Image(uiImage: img)
34 | .resizable()
35 | })
36 | .aspectRatio(1, contentMode: .fit)
37 | .frame(width: 50)
38 |
39 | VStack(alignment: .leading) {
40 | Text(skill.name)
41 | // Spacer()
42 | Text(skill.description)
43 | .font(.system(size: 10))
44 | .lineLimit(3)
45 | }
46 | Spacer()
47 | VStack {
48 | Text("等级")
49 | Text("\(skill.level)")
50 | .font(.system(size: 16))
51 | .bold()
52 | }
53 | }
54 | .frame(height: 60)
55 | }
56 | }
57 |
58 |
--------------------------------------------------------------------------------
/chafenqi/View/v2/Info/ChunithmList/InfoChunithmTicketList.swift:
--------------------------------------------------------------------------------
1 | //
2 | // InfoChunithmTicketList.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2023/8/31.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct InfoChunithmTicketList: View {
11 | var tickets: [UserChunithmTicketEntry]
12 |
13 | var body: some View {
14 | Form {
15 | Section {
16 | ForEach(tickets, id: \.name) { ticket in
17 | InfoChunithmTicketItem(ticket: ticket)
18 | }
19 | } header: {
20 | Text("共\(tickets.reduce(0) { $0 + $1.count })张功能票")
21 | }
22 | }
23 | .navigationTitle("功能票列表")
24 | .navigationBarTitleDisplayMode(.inline)
25 | }
26 | }
27 |
28 | struct InfoChunithmTicketItem: View {
29 | @Environment(\.managedObjectContext) var context
30 | var ticket: UserChunithmTicketEntry
31 |
32 | var body: some View {
33 | HStack {
34 | AsyncImage(url: URL(string: ticket.url)!, context: context, placeholder: {
35 | ProgressView()
36 | }, image: { img in
37 | Image(uiImage: img)
38 | .resizable()
39 | })
40 | .aspectRatio(contentMode: .fit)
41 | .frame(width: 50)
42 |
43 | VStack(alignment: .leading) {
44 | Text(ticket.name)
45 | Text(ticket.description)
46 | .font(.system(size: 10))
47 | .lineLimit(3)
48 | }
49 | Spacer()
50 | VStack {
51 | Text("数量")
52 | Text("\(ticket.count)")
53 | .font(.system(size: 16))
54 | .bold()
55 | }
56 | }
57 | .frame(height: 60)
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/chafenqi/View/v2/Info/ChunithmList/InfoChunithmTrophyList.swift:
--------------------------------------------------------------------------------
1 | //
2 | // InfoChunithmTrophyList.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2023/7/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct InfoChunithmTrophyList: View {
11 | var list = [UserChunithmTrophyEntry]()
12 | let typeStrings = [
13 | "normal": "普通称号",
14 | "copper": "铜称号",
15 | "silver": "银称号",
16 | "gold": "金称号",
17 | "platinum": "白金称号"
18 | ]
19 |
20 | @State var group = [String: [UserChunithmTrophyEntry]]()
21 |
22 | var body: some View {
23 | Form {
24 | ForEach(Array(group.keys).sorted(by: { $0.chunithmTrophySignificance < $1.chunithmTrophySignificance }), id: \.hashValue) { key in
25 | Section {
26 | ForEach(group[key]!, id: \.name) { trophy in
27 | HStack {
28 | Spacer()
29 | VStack(alignment: .center) {
30 | Text(trophy.name)
31 | .bold()
32 | .multilineTextAlignment(.center)
33 |
34 | Text(trophy.description)
35 | .multilineTextAlignment(.center)
36 | }
37 | Spacer()
38 | }
39 | }
40 | } header: {
41 | Text("\(typeStrings[key] ?? "普通") 共\(group[key]?.count ?? 0)个")
42 | }
43 | }
44 | }
45 | .onAppear {
46 | group = Dictionary(grouping: list, by: {
47 | $0.type
48 | })
49 | }
50 | .navigationTitle("称号一览")
51 | .navigationBarTitleDisplayMode(.inline)
52 | }
53 | }
54 |
55 | struct InfoChunithmTrophyList_Previews: PreviewProvider {
56 | static var previews: some View {
57 | InfoChunithmTrophyList()
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/chafenqi/View/v2/Info/MaimaiList/InfoMaimaiCharacterList.swift:
--------------------------------------------------------------------------------
1 | //
2 | // InfoCharacterList.swift
3 | // chafenqi
4 | //
5 | // Created by xinyue on 2023/6/14.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct InfoMaimaiCharacterList: View {
11 | var list: [UserMaimaiCharacterEntry]
12 |
13 | @State var group = [String: [UserMaimaiCharacterEntry]]()
14 |
15 | var body: some View {
16 | Form {
17 | ForEach(Array(group.keys).sorted(), id: \.hashValue) { area in
18 | Section {
19 | ForEach(group[area]!, id: \.url) { char in
20 | InfoMaimaiCharacterEntry(char: char)
21 | }
22 | } header: {
23 | Text(area)
24 | }
25 | }
26 | }
27 | .navigationTitle("角色一览")
28 | .navigationBarTitleDisplayMode(.inline)
29 | .onAppear {
30 | group = Dictionary(grouping: list, by: {
31 | $0.area
32 | })
33 | }
34 | }
35 | }
36 |
37 |
38 | struct InfoMaimaiCharacterEntry: View {
39 | @Environment(\.managedObjectContext) var context
40 | var char: UserMaimaiCharacterEntry
41 |
42 | @State var image = UIImage()
43 |
44 | var body: some View {
45 | HStack {
46 | AsyncImage(url: URL(string: char.url)!, context: context, placeholder: {
47 | ProgressView()
48 | }, image: { img in
49 | let _ = DispatchQueue.main.async {
50 | self.image = img
51 | }
52 | Image(uiImage: img)
53 | .resizable()
54 | })
55 | .aspectRatio(contentMode: .fit)
56 | .mask(RoundedRectangle(cornerRadius: 5))
57 | .frame(width: 60, height: 60)
58 | .contextMenu {
59 | Button {
60 | UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
61 | } label: {
62 | Label("保存到相册", systemImage: "square.and.arrow.down")
63 | }
64 | }
65 | VStack {
66 | HStack {
67 | Text(char.name)
68 | .bold()
69 | Spacer()
70 | Text("\(char.level)")
71 | }
72 | }
73 | }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/chafenqi/View/v2/Info/MaimaiList/InfoMaimaiFrameList.swift:
--------------------------------------------------------------------------------
1 | //
2 | // InfoMaimaiFrameList.swift
3 | // chafenqi
4 | //
5 | // Created by xinyue on 2023/6/14.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct InfoMaimaiFrameList: View {
11 | @Environment(\.managedObjectContext) var context
12 | var list: [UserMaimaiFrameEntry]
13 |
14 | @State var group = [String: [UserMaimaiFrameEntry]]()
15 |
16 | var body: some View {
17 | Form {
18 | ForEach(Array(group.keys).sorted(), id: \.hashValue) { area in
19 | Section {
20 | ForEach(group[area]!, id: \.url) { frame in
21 | InfoMaimaiFrameEntry(frame: frame)
22 | }
23 | } header: {
24 | Text(area)
25 | }
26 | }
27 | }
28 | .navigationTitle("底板一览")
29 | .navigationBarTitleDisplayMode(.inline)
30 | .onAppear {
31 | group = Dictionary(grouping: list, by: {
32 | $0.area
33 | })
34 | }
35 | }
36 | }
37 |
38 | struct InfoMaimaiFrameEntry: View {
39 | @Environment(\.managedObjectContext) var context
40 | var frame: UserMaimaiFrameEntry
41 |
42 | @State var image = UIImage()
43 |
44 | var body: some View {
45 | VStack {
46 | AsyncImage(url: URL(string: frame.url)!, context: context, placeholder: {
47 | ProgressView()
48 | }, image: { img in
49 | Image(uiImage: img)
50 | .resizable()
51 |
52 | })
53 | .aspectRatio(contentMode: .fit)
54 | .contextMenu {
55 | Button {
56 | UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
57 | } label: {
58 | Label("保存到相册", systemImage: "square.and.arrow.down")
59 | }
60 | }
61 | Text(frame.name)
62 | .bold()
63 | Text(frame.description)
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/chafenqi/View/v2/Info/MaimaiList/InfoMaimaiNameplateList.swift:
--------------------------------------------------------------------------------
1 | //
2 | // InfoMaimaiNameplateList.swift
3 | // chafenqi
4 | //
5 | // Created by xinyue on 2023/6/14.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct InfoMaimaiNameplateList: View {
11 | @Environment(\.managedObjectContext) var context
12 | var list: [UserMaimaiNameplateEntry]
13 |
14 | @State private var image = [String: UIImage]()
15 |
16 | var body: some View {
17 | Form {
18 | ForEach(list, id: \.url) { nameplate in
19 | VStack {
20 | AsyncImage(url: URL(string: nameplate.url)!, context: context, placeholder: {
21 | ProgressView()
22 | }, image: { img in
23 | let _ = DispatchQueue.main.async {
24 | self.image[nameplate.url] = img
25 | }
26 | Image(uiImage: img)
27 | .resizable()
28 |
29 | })
30 | .aspectRatio(contentMode: .fit)
31 | .frame(height: 60)
32 | .contextMenu {
33 | Button {
34 | if let image = self.image[nameplate.url] {
35 | UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
36 | }
37 | } label: {
38 | Label("保存到相册", systemImage: "square.and.arrow.down")
39 | }
40 | }
41 | Text(nameplate.name)
42 | .bold()
43 | Text(nameplate.description)
44 | }
45 | }
46 | }
47 | .navigationTitle("名牌版一览")
48 | .navigationBarTitleDisplayMode(.inline)
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/chafenqi/View/v2/Info/MaimaiList/InfoMaimaiTrophyList.swift:
--------------------------------------------------------------------------------
1 | //
2 | // InfoMaimaiTrophyList.swift
3 | // chafenqi
4 | //
5 | // Created by xinyue on 2023/6/14.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct InfoMaimaiTrophyList: View {
11 | @Environment(\.managedObjectContext) var context
12 | var list: [UserMaimaiTrophyEntry]
13 |
14 | let typeStrings = [
15 | "NORMAL": "普通称号",
16 | "BRONZE": "铜称号",
17 | "SILVER": "银称号",
18 | "GOLD": "金称号",
19 | "RAINBOW": "彩虹称号"
20 | ]
21 |
22 | @State var group = [String: [UserMaimaiTrophyEntry]]()
23 |
24 | var body: some View {
25 | Form {
26 | ForEach(Array(group.keys).sorted(by: { a, b in a.significance < b.significance }), id: \.hashValue) { type in
27 | Section {
28 | ForEach(group[type]!, id: \.name) { trophy in
29 | HStack {
30 | Spacer()
31 | VStack(alignment: .center) {
32 | Text(trophy.name)
33 | .bold()
34 | .multilineTextAlignment(.center)
35 |
36 | Text(trophy.description)
37 | .multilineTextAlignment(.center)
38 | }
39 | Spacer()
40 | }
41 | }
42 | } header: {
43 | Text("\(typeStrings[type] ?? "普通称号") 共\(group[type]?.count ?? 0)个")
44 | }
45 | }
46 |
47 | }
48 | .navigationTitle("称号一览")
49 | .navigationBarTitleDisplayMode(.inline)
50 | .onAppear {
51 | group = Dictionary(grouping: list, by: {
52 | $0.type
53 | })
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/chafenqi/View/v2/Leaderboard/LeaderboardTabView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LeaderboardTabView.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2024/06/29.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct LeaderboardTabView: View {
11 | var proxy: ScrollViewProxy
12 | @Binding var currentIndex: Int
13 | @Namespace var namespace
14 |
15 | let items = [
16 | TabBarItem(title: "Rating", unselectedIcon: "chart.bar", selectedIcon: "chart.bar.fill"),
17 | TabBarItem(title: "总分", unselectedIcon: "chart.bar.doc.horizontal", selectedIcon: "chart.bar.doc.horizontal.fill"),
18 | TabBarItem(title: "游玩曲目数", unselectedIcon: "chart.pie", selectedIcon: "chart.pie.fill"),
19 | TabBarItem(title: "榜一取得数", unselectedIcon: "1.circle", selectedIcon: "1.circle.fill")
20 | ]
21 |
22 | var body: some View {
23 | ScrollView(.horizontal, showsIndicators: false) {
24 | HStack {
25 | ForEach(Array(zip(items.indices, items)), id: \.0) { index, item in
26 | LeaderboardTabBarComponent(currentIndex: $currentIndex, proxy: proxy, namespace: namespace.self, index: index, title: item.title, unselectedIcon: item.unselectedIcon, selectedIcon: item.selectedIcon)
27 | }
28 | }
29 | }
30 | .frame(height: 60)
31 | }
32 | }
33 |
34 | struct LeaderboardTabBarComponent: View {
35 | @Environment(\.colorScheme) var colorScheme
36 |
37 | @Binding var currentIndex: Int
38 | var proxy: ScrollViewProxy
39 | let namespace: Namespace.ID
40 |
41 | var index: Int
42 | var title: String
43 | var unselectedIcon: String
44 | var selectedIcon: String
45 |
46 | var body: some View {
47 | Button {
48 | withAnimation(.spring) {
49 | currentIndex = index
50 | proxy.scrollTo(index)
51 | }
52 | } label: {
53 | VStack {
54 | Spacer()
55 | HStack {
56 | Image(systemName: currentIndex == index ? selectedIcon : unselectedIcon)
57 | Text(title)
58 | }
59 | .padding(.horizontal)
60 | if currentIndex == index {
61 | (colorScheme == .light ? Color.black : Color.white)
62 | .frame(height: 2)
63 | .matchedGeometryEffect(id: "underline", in: namespace, properties: .frame)
64 | } else {
65 | Color.clear
66 | .frame(height: 2)
67 | }
68 | }
69 | .animation(.spring, value: currentIndex)
70 | }
71 | .buttonStyle(.plain)
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/chafenqi/View/v2/Rating/RatingShareView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RatingShareView.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2024/09/30.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct RatingShareView: View {
11 | var type: String
12 |
13 | @ObservedObject var user: CFQNUser
14 | @State private var doneLoading = false
15 | @State private var image: UIImage? = nil
16 |
17 | var body: some View {
18 | VStack {
19 | if let image = image {
20 | Image(uiImage: image)
21 | .resizable()
22 | .aspectRatio(contentMode: .fit)
23 | .cornerRadius(10.0)
24 | Spacer()
25 | HStack {
26 | Button {
27 | shareImage(image: image)
28 | } label: {
29 | Label("分享", systemImage: "square.and.arrow.up")
30 | }
31 | }
32 | } else {
33 | ProgressView(label: {
34 | Text("生成图片中...")
35 | })
36 | }
37 | }
38 | .navigationTitle(type == "b30" ? "B30分表" : "B50分表")
39 | .navigationBarTitleDisplayMode(.inline)
40 | .padding()
41 | .onAppear {
42 | image = nil
43 | if user.currentMode == 0 {
44 | fetchB30Image()
45 | } else {
46 | fetchB50Image()
47 | }
48 | }
49 | }
50 |
51 | func fetchB30Image() {
52 | Task {
53 | image = await CFQImageServer.getChunithmB30Image(authToken: user.jwtToken)
54 | if let b30Image = image {
55 | image = b30Image
56 | }
57 | }
58 | }
59 |
60 | func fetchB50Image() {
61 | Task {
62 | image = await CFQImageServer.getMaimaiB50Image(authToken: user.jwtToken)
63 | if let b50Image = image {
64 | image = b50Image
65 | }
66 | }
67 | }
68 |
69 | func shareImage(image: UIImage) {
70 | if let viewController = UIApplication.shared.windows.first?.rootViewController {
71 | let activityViewController = UIActivityViewController(activityItems: [image], applicationActivities: nil)
72 | activityViewController.popoverPresentationController?.sourceView = viewController.view
73 | activityViewController.popoverPresentationController?.sourceRect = .zero
74 | UIApplication.shared.windows.first?.rootViewController?.present(activityViewController, animated: true)
75 | }
76 | }
77 | }
78 |
79 |
--------------------------------------------------------------------------------
/chafenqi/View/v2/RootView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RootView.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2023/5/7.
6 | //
7 |
8 | import SwiftUI
9 | import AlertToast
10 |
11 | struct RootView: View {
12 | @ObservedObject var user: CFQNUser
13 | @ObservedObject var alertToast = AlertToastModel.shared
14 |
15 | @State var loadingCache = false
16 |
17 | var body: some View {
18 | VStack {
19 | if (loadingCache) {
20 | VStack {
21 | Image("Icon")
22 | .resizable()
23 | .aspectRatio(1, contentMode: .fit)
24 | .frame(width: 100)
25 | .mask(RoundedRectangle(cornerRadius: 10))
26 | .overlay(RoundedRectangle(cornerRadius: 10).stroke(AngularGradient(gradient: Gradient(colors: [.red, .yellow, .green, .blue, .purple, .red]), center: .center), lineWidth: 3))
27 | .padding(.bottom)
28 | ProgressView(user.loadPrompt)
29 | }
30 | } else if (user.didLogin) {
31 | TabView() {
32 | NavigationView {
33 | HomeView(user: user)
34 | }
35 | .tabItem {
36 | Image(systemName: "house.fill")
37 | Text("主页")
38 | }
39 | .tag(TabIdentifier.home)
40 | .navigationViewStyle(.stack)
41 |
42 | NavigationView {
43 | UpdaterRootView(user: user)
44 | }
45 | .tabItem {
46 | Image(systemName: "paperplane")
47 | Text("传分")
48 | }
49 | .navigationViewStyle(.stack)
50 |
51 | NavigationView {
52 | SongListView(user: user)
53 | }
54 | .tabItem {
55 | Image(systemName: "music.note.list")
56 | Text("歌曲")
57 | }
58 | }
59 | .toast(isPresenting: $alertToast.show, duration: 5, tapToDismiss: true) {
60 | alertToast.toast
61 | }
62 | } else {
63 | LoginView(user: user)
64 | }
65 | }
66 | .onAppear {
67 | if (!user.jwtToken.isEmpty) {
68 | // Already logged in
69 | loadingCache = true
70 | Task {
71 | do {
72 | try await user.loadFromCache()
73 | withAnimation() {
74 | user.didLogin = true
75 | loadingCache = false
76 | }
77 | } catch {
78 | print("[LoginView] Cannot load cache for", user.username, error)
79 | loadingCache = false
80 | user.didLogin = false
81 | }
82 | }
83 | }
84 | }
85 | }
86 | }
87 |
88 | struct RootView_Previews: PreviewProvider {
89 | static var previews: some View {
90 | RootView(user: CFQNUser())
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/chafenqi/View/v2/Settings/LogView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LogView.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2023/9/4.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct LogView: View {
11 | @State private var logger = Logger.shared
12 |
13 | var body: some View {
14 | List {
15 | ForEach(logger.getAllLogs().reversed(), id: \.timestamp) { log in
16 | VStack(alignment: .leading) {
17 | Text(log.timestamp.toDateString(format: "yyyy-MM-dd HH:mm:ss") + " " + log.level.rawValue)
18 | .foregroundColor(.gray)
19 | .font(.system(size: 12))
20 | Text(log.message)
21 | }
22 | }
23 | }
24 | .navigationTitle("调试输出")
25 | .navigationBarTitleDisplayMode(.inline)
26 | }
27 | }
28 |
29 | struct LogView_Previews: PreviewProvider {
30 | static var previews: some View {
31 | LogView()
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/chafenqi/View/v2/Settings/PerksPreview/PerkSheetView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PerkSheetView.swift
3 | // chafenqi
4 | //
5 | // Created by xinyue on 2023/6/26.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct PerkSheetView: View {
11 | @State private var selection = ""
12 | @State private var title = ""
13 | @State private var description = ""
14 |
15 | let names = ["playerinfo", "delta", "history"]
16 | let altNames = ["alt_playerinfo", "alt_delta", ""]
17 |
18 | var body: some View {
19 | VStack {
20 | TabView(selection: $selection) {
21 | ForEach(Array(names.enumerated()), id: \.offset) { index, name in
22 | PerkCardView(name: name, altName: altNames[index])
23 | .tag(name)
24 | }
25 | }
26 | .tabViewStyle(.page(indexDisplayMode: .automatic))
27 |
28 | VStack(alignment: .center) {
29 | Text(title)
30 | .bold()
31 | .font(.system(size: 20))
32 | .padding(.bottom)
33 | Text(description)
34 | .multilineTextAlignment(.center)
35 | .padding([.bottom, .horizontal])
36 |
37 | }
38 | }
39 | .onAppear {
40 | selection = "playerinfo"
41 | }
42 | .onChange(of: selection) { newValue in
43 | withAnimation {
44 | switch selection {
45 | case "delta":
46 | title = perks_delta_title
47 | description = perks_delta_description
48 | case "history":
49 | title = perks_history_title
50 | description = perks_history_description
51 | case "playerinfo":
52 | title = perks_playerInfo_title
53 | description = perks_playerInfo_description
54 | default:
55 | break
56 | }
57 | }
58 | }
59 | }
60 | }
61 |
62 | struct PerkCardView: View {
63 | var name: String
64 | var altName: String
65 |
66 | @State private var flipped = false
67 |
68 | var body: some View {
69 | ZStack {
70 | Image(flipped ? altName : name)
71 | .resizable()
72 | .aspectRatio(contentMode: .fit)
73 | .mask(RoundedRectangle(cornerRadius: 15))
74 | .shadow(radius: 5)
75 | .padding(30)
76 | .rotation3DEffect(flipped ? Angle(degrees: 180) : Angle(degrees: 0), axis: (x: CGFloat(0), y: CGFloat(-1), z: CGFloat(0)))
77 | .onTapGesture {
78 | if !altName.isEmpty {
79 | withAnimation {
80 | flipped.toggle()
81 | }
82 | }
83 | }
84 | }
85 | .rotation3DEffect(flipped ? Angle(degrees: 180) : Angle(degrees: 0), axis: (x: CGFloat(0), y: CGFloat(-1), z: CGFloat(0)))
86 | }
87 | }
88 |
89 | struct PerkSheetView_Previews: PreviewProvider {
90 | static var previews: some View {
91 | PerkSheetView()
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/chafenqi/View/v2/Settings/PerksPreview/PerksString.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PerksString.swift
3 | // chafenqi
4 | //
5 | // Created by xinyue on 2023/6/26.
6 | //
7 |
8 | import Foundation
9 |
10 | let perks_quicklook_title = "Rating快速预览"
11 | let perks_history_title = "游玩记录"
12 | let perks_playerInfo_title = "玩家信息"
13 | let perks_delta_title = "出勤记录"
14 |
15 | let perks_quicklook_description = "通过滑动来快速预览和定位你的Rating成绩"
16 | let perks_history_description = "查询和比较已记录的单曲游玩信息"
17 | let perks_playerInfo_description = "全面查看玩家信息、底板、名牌、已获得的伙伴和分析歌曲游玩总体情况"
18 | let perks_delta_description = "精确到每日的出勤详细记录、数据变化和趋势分析"
19 |
--------------------------------------------------------------------------------
/chafenqi/View/v2/Settings/RedeemView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RedeemView.swift
3 | // chafenqi
4 | //
5 | // Created by xinyue on 2023/5/26.
6 | //
7 |
8 | import SwiftUI
9 | import AlertToast
10 |
11 | let perks =
12 | """
13 | 通过兑换订阅服务,您可以获得以下功能:
14 | - 详细的用户数据面板
15 | - 出勤数据记录
16 | - Rating历史趋势
17 | - 单曲游玩记录及成绩趋势
18 | - 小组件布局自定义
19 | - 国服排行榜
20 | - 创建并管理团队
21 |
22 | 您可以通过在爱发电赞助指定方案以获得订阅服务兑换码。
23 | """
24 |
25 | struct RedeemView: View {
26 | @ObservedObject var user: CFQNUser
27 |
28 | @State var toastModel = AlertToastModel.shared
29 | @State var code = ""
30 | @State var isVerifying = false
31 | @State var isShowingPreview = false
32 |
33 | let successToast = AlertToast(displayMode: .hud, type: .complete(.green), title: "兑换成功", subTitle: "刷新即可生效")
34 | let failureToast = AlertToast(displayMode: .hud, type: .error(.red), title: "兑换码无效", subTitle: "请检查是否输入错误")
35 |
36 | var body: some View {
37 | Form {
38 | Section {
39 | TextField("输入兑换码", text: $code)
40 | .autocapitalization(.none)
41 | Button {
42 | toastModel.show = false
43 | isVerifying.toggle()
44 | Task {
45 | await verify()
46 | }
47 | } label: {
48 | HStack {
49 | Text("兑换")
50 | if isVerifying {
51 | Spacer()
52 | ProgressView()
53 | }
54 | }
55 | }
56 | .disabled(isVerifying)
57 | }
58 |
59 | Section {
60 | Link("获取兑换码...", destination: URL(string: "https://afdian.com/a/chafenqi")!)
61 | NavigationLink {
62 | NotPremiumView()
63 | } label: {
64 | Text("了解详细功能")
65 | }
66 | } footer: {
67 | Text(perks)
68 | }
69 | }
70 | .navigationTitle(user.isPremium ? "续费会员" : "加入会员")
71 | .navigationBarTitleDisplayMode(.inline)
72 | .sheet(isPresented: $isShowingPreview) {
73 | PerkSheetView()
74 | }
75 | }
76 |
77 | // MARK: Verify Code
78 | func verify() async {
79 | do {
80 | let result = try await CFQUserServer.redeem(authToken: user.jwtToken, code: code)
81 | if result.isEmpty {
82 | toastModel.toast = successToast
83 | } else {
84 | toastModel.toast = failureToast
85 | }
86 | } catch {
87 | toastModel.toast = failureToast
88 | }
89 | isVerifying = false
90 | }
91 | }
92 |
93 |
--------------------------------------------------------------------------------
/chafenqi/View/v2/Settings/SettingsHomeArrangement.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SettingsHomeArrangement.swift
3 | // chafenqi
4 | //
5 | // Created by xinyue on 2023/6/6.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct SettingsHomeArrangement: View {
11 | @AppStorage("settingsHomeArrangement") var homeArrangement = "最近动态|Rating分析|出勤记录|排行榜"
12 | @State private var editMode = EditMode.active
13 |
14 | @State private var homeModules = [String]()
15 |
16 | var body: some View {
17 | List {
18 | ForEach(homeModules, id: \.hashValue) { value in
19 | Text(value)
20 | }
21 | .onMove { index, newIndex in
22 | homeModules.move(fromOffsets: index, toOffset: newIndex)
23 | }
24 |
25 | }
26 | .environment(\.editMode, $editMode)
27 | .navigationTitle("主页排序")
28 | .navigationBarTitleDisplayMode(.inline)
29 | .onAppear {
30 | homeModules = homeArrangement.components(separatedBy: "|")
31 | }
32 | .onDisappear {
33 | homeArrangement = homeModules.joined(separator: "|")
34 | }
35 | }
36 | }
37 |
38 | struct SettingsHomeArrangement_Previews: PreviewProvider {
39 | static var previews: some View {
40 | SettingsHomeArrangement()
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/chafenqi/View/v2/Settings/SponsorView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SponsorView.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2023/3/20.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct SponsorView: View {
11 | @State var sponsorList: [String] = []
12 |
13 | var body: some View {
14 | VStack {
15 | Form {
16 | Section {
17 | TextInfoView(text: "SoreHait", info: "代码级指导")
18 | TextInfoView(text: "bakapiano", info: "国服代理传分方案")
19 | TextInfoView(text: "Diving-Fish", info: "数据支持")
20 | TextInfoView(text: "sdvx.in", info: "中二侧谱面数据")
21 | } header: {
22 | Text("技术支持")
23 | }
24 |
25 | Section {
26 | TextInfoView(text: "0Shu", info: "App图标设计")
27 | TextInfoView(text: "C.C.Duck", info: "配色方案")
28 | } header: {
29 | Text("美术支持")
30 | }
31 |
32 | Section {
33 | ForEach(sponsorList.unique, id: \.hashValue) { sponsor in
34 | Text(sponsor)
35 | }
36 | } header: {
37 | Text("爱发电赞助人员")
38 | } footer: {
39 | Text("排名不分先后,默认显示爱发电昵称")
40 | }
41 | }
42 | }
43 | .navigationTitle("鸣谢")
44 | .navigationBarTitleDisplayMode(.inline)
45 | .onAppear {
46 | Task {
47 | do {
48 | let request = URLRequest(url: URL(string: "\(CFQServer.serverAddress)api/stat/sponsor")!)
49 | let (data, _) = try await URLSession.shared.data(for: request)
50 | sponsorList = try JSONDecoder().decode(Array.self, from: data)
51 | sponsorList.reverse()
52 | } catch {
53 | print(error)
54 | sponsorList.append("加载出错")
55 | }
56 | }
57 | }
58 | }
59 | }
60 |
61 | struct SponsorView_Previews: PreviewProvider {
62 | static var previews: some View {
63 | SponsorView()
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/chafenqi/View/v2/Song/Charts/SongScoreTrendChart.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SongScoreTrendChart.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2023/6/18.
6 | //
7 |
8 | import SwiftUI
9 | import SwiftUICharts
10 |
11 | struct SongScoreTrendChart: View {
12 | @Binding var rawDataPoints: [(Double, String)]
13 | var mode = 0
14 | @Binding var shouldShowPointMarkers: Bool
15 |
16 | var body: some View {
17 | let chartData = addDataPoints()
18 | VStack {
19 | if shouldShowPointMarkers {
20 | LineChart(chartData: chartData)
21 | .pointMarkers(chartData: chartData)
22 | .touchOverlay(chartData: chartData, specifier: mode == 0 ? "%0.f" : "%.4f", unit: mode == 0 ? .none : .suffix(of: "%"))
23 | .yAxisGrid(chartData: chartData)
24 | .yAxisLabels(chartData: chartData, specifier: mode == 0 ? "%0.f" : "%.2f")
25 | .floatingInfoBox(chartData: chartData)
26 | .headerBox(chartData: chartData)
27 | .transaction { transaction in
28 | transaction.animation = nil
29 | }
30 | .id(UUID())
31 | } else {
32 | LineChart(chartData: chartData)
33 | .touchOverlay(chartData: chartData, specifier: mode == 0 ? "%0.f" : "%.4f", unit: mode == 0 ? .none : .suffix(of: "%"))
34 | .yAxisGrid(chartData: chartData)
35 | .yAxisLabels(chartData: chartData, specifier: mode == 0 ? "%0.f" : "%.2f")
36 | .floatingInfoBox(chartData: chartData)
37 | .headerBox(chartData: chartData)
38 | .transaction { transaction in
39 | transaction.animation = nil
40 | }
41 | .id(UUID())
42 | }
43 | }
44 | }
45 |
46 | func addDataPoints() -> LineChartData {
47 | var dataPoints = [LineChartDataPoint]()
48 | for (point, description) in rawDataPoints {
49 | dataPoints.append(LineChartDataPoint(value: point, xAxisLabel: description, description: description))
50 | }
51 | let data = LineDataSet(
52 | dataPoints: dataPoints,
53 | legendTitle: "成绩",
54 | pointStyle: .init(),
55 | style: .init(lineColour: .init(colour: .blue), lineType: .curvedLine)
56 | )
57 | let metadata = ChartMetadata(title: "成绩趋势", subtitle: "全部数据")
58 | let chartStyle = LineChartStyle(
59 | infoBoxPlacement: .floating,
60 | infoBoxBorderColour: Color.primary,
61 | infoBoxBorderStyle: StrokeStyle(lineWidth: 1),
62 | markerType: .indicator(style: .init()),
63 | baseline: .minimumValue,
64 | topLine: .maximumValue
65 | )
66 | return LineChartData(dataSets: data, metadata: metadata, chartStyle: chartStyle)
67 | }
68 | }
69 |
70 |
--------------------------------------------------------------------------------
/chafenqi/View/v2/Song/List/Filter/MultiplePicker.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MultiplePicker.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2024/08/27.
6 | //
7 |
8 | import Foundation
9 | import SwiftUI
10 |
11 | struct MultiplePicker: View {
12 | var title: String
13 | var options: [String]
14 | @Binding var selections: [String]
15 |
16 | var body: some View {
17 | NavigationLink {
18 | Form {
19 | ForEach(Array(options.enumerated()), id: \.offset) { (index, option) in
20 | MultiplePickerItem(option: option, selections: $selections)
21 | }
22 | }
23 | } label: {
24 | HStack {
25 | Text(title)
26 | Spacer()
27 | if selections.count == 1 {
28 | Text(selections.first ?? "")
29 | .foregroundColor(.gray)
30 | } else if selections.count > 1 {
31 | Text("已选择\(selections.count)项")
32 | .foregroundColor(.gray)
33 | }
34 | }
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/chafenqi/View/v2/Song/List/Filter/MultiplePickerItem.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MultiplePickerItem.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2024/08/27.
6 | //
7 |
8 | import Foundation
9 | import SwiftUI
10 |
11 | struct MultiplePickerItem: View {
12 | var option: String
13 | @State var isSelected: Bool = false
14 | @Binding var selections: [String]
15 |
16 | var body: some View {
17 | Button {
18 | if selections.contains(option) {
19 | selections.removeAll { entry in entry == option }
20 | isSelected = false
21 | } else {
22 | selections.append(option)
23 | isSelected = true
24 | }
25 | } label: {
26 | HStack {
27 | Text(option)
28 | Spacer()
29 | if selections.contains(option) {
30 | Image(systemName: "checkmark")
31 | }
32 | }
33 | .contentShape(Rectangle())
34 | }
35 | .buttonStyle(.plain)
36 | .onAppear {
37 | if selections.contains(option) {
38 | isSelected = true
39 | } else {
40 | isSelected = false
41 | }
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/chafenqi/View/v2/Song/SongChartView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SongChartView.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2023/1/21.
6 | //
7 |
8 | import SwiftUI
9 | import CachedAsyncImage
10 |
11 | struct SongChartView: View {
12 |
13 | @State var chartImage: UIImage
14 |
15 | var body: some View {
16 | ScrollView(.horizontal) {
17 | Image(uiImage: chartImage)
18 | .resizable()
19 | .scaledToFill()
20 | .background(Color.black)
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/chafenqi/View/v2/Song/SongCoverView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SongCoverView.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2023/2/3.
6 | //
7 |
8 | import SwiftUI
9 | import CoreData
10 |
11 | struct SongCoverView: View {
12 | @Environment(\.colorScheme) var colorScheme
13 | @Environment(\.managedObjectContext) var context
14 |
15 | var coverURL: URL
16 | var size: CGFloat
17 | var cornerRadius: CGFloat
18 | var withShadow = true
19 | var switchShadowColor = false
20 | var diffColor: Color?
21 | var rainbowStroke = false
22 |
23 | var body: some View {
24 | ZStack {
25 | if (withShadow) {
26 | AsyncImage(url: coverURL, context: context, placeholder: {
27 | ProgressView()
28 | }, image: {
29 | Image(uiImage: $0)
30 | .resizable()
31 | })
32 | // .scaledToFill()
33 | .frame(width: size, height: size)
34 | .cornerRadius(cornerRadius)
35 | .shadow(color: switchShadowColor ? (colorScheme == .dark ? Color.white.opacity(0.33) : Color.black.opacity(0.33)) : Color.black.opacity(0.33), radius: 5)
36 | } else {
37 | AsyncImage(url: coverURL, context: context, placeholder: {
38 | ProgressView()
39 | }, image: {
40 | Image(uiImage: $0)
41 | .resizable()
42 | })
43 | // .scaledToFill()
44 | .frame(width: size, height: size)
45 | .cornerRadius(cornerRadius)
46 | }
47 |
48 |
49 | if let color = diffColor {
50 | RoundedRectangle(cornerRadius: cornerRadius)
51 | .strokeBorder(color, lineWidth: 2)
52 | .frame(width: size, height: size)
53 | } else if rainbowStroke {
54 | RoundedRectangle(cornerRadius: cornerRadius)
55 | .stroke(AngularGradient(gradient: Gradient(colors: [.red, .yellow, .green, .blue, .purple, .red]), center: .center), lineWidth: 2)
56 | .frame(width: size, height: size)
57 | }
58 | }
59 | .id(UUID())
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/chafenqi/View/v2/Song/Stats/SongStatsTabBarView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SongStatsTabBarView.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2024/06/17.
6 | //
7 |
8 | import Foundation
9 | import SwiftUI
10 |
11 | struct TabBarItem {
12 | var title: String
13 | var unselectedIcon: String
14 | var selectedIcon: String
15 | }
16 |
17 | struct TabBarView: View {
18 | @Binding var currentIndex: Int
19 | @Namespace var namespace
20 |
21 | var items = [
22 | TabBarItem(title: "排行榜", unselectedIcon: "chart.bar", selectedIcon: "chart.bar.fill"),
23 | TabBarItem(title: "统计信息", unselectedIcon: "chart.pie", selectedIcon: "chart.pie.fill"),
24 | TabBarItem(title: "游玩记录", unselectedIcon: "clock", selectedIcon: "clock.fill")
25 | ]
26 |
27 | var body: some View {
28 | HStack {
29 | ForEach(Array(zip(items.indices, items)), id: \.0) { index, item in
30 | TabBarComponent(currentIndex: $currentIndex, namespace: namespace.self, index: index, title: item.title, unselectedIcon: item.unselectedIcon, selectedIcon: item.selectedIcon)
31 | }
32 | }
33 | .padding(.horizontal)
34 | .frame(height: 60)
35 | }
36 | }
37 |
38 | struct TabBarComponent: View {
39 | @Environment(\.colorScheme) var colorScheme
40 |
41 | @Binding var currentIndex: Int
42 | let namespace: Namespace.ID
43 |
44 | var index: Int
45 | var title: String
46 | var unselectedIcon: String
47 | var selectedIcon: String
48 |
49 | var body: some View {
50 | Button {
51 | withAnimation(.spring) {
52 | currentIndex = index
53 | }
54 | } label: {
55 | VStack {
56 | Spacer()
57 | HStack {
58 | Image(systemName: currentIndex == index ? selectedIcon : unselectedIcon)
59 | Text(title)
60 | }
61 | if currentIndex == index {
62 | (colorScheme == .light ? Color.black : Color.white)
63 | .frame(height: 2)
64 | .matchedGeometryEffect(id: "underline", in: namespace, properties: .frame)
65 | } else {
66 | Color.clear
67 | .frame(height: 2)
68 | }
69 | }
70 | .animation(.spring, value: currentIndex)
71 | }
72 | .buttonStyle(.plain)
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/chafenqi/View/v2/Team/Info/TeamActivityView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TeamActivityView.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2024/12/25.
6 | //
7 |
8 | import Foundation
9 | import SwiftUI
10 | import CachedAsyncImage
11 |
12 | struct TeamActivityView: View {
13 | @ObservedObject var team: CFQTeam
14 |
15 | var body: some View {
16 | ScrollView(.vertical) {
17 | LazyVStack {
18 | ForEach(team.current.activities.sorted(by: { $0.timestamp > $1.timestamp }), id: \.id) { activity in
19 | if let member = team.current.members.first(where: { $0.userId == activity.userId }) {
20 | TeamActivityEntryView(activity: activity, member: member)
21 | }
22 | }
23 | }
24 | }
25 | }
26 | }
27 |
28 | struct TeamActivityEntryView: View {
29 | var activity: TeamActivity
30 | var member: TeamMember
31 |
32 | var body: some View {
33 | HStack {
34 | CachedAsyncImage(url: URL(string: member.avatar)) { image in
35 | image
36 | .resizable()
37 | .cornerRadius(5)
38 | .aspectRatio(contentMode: .fit)
39 | } placeholder: {
40 | ProgressView()
41 | }
42 | .frame(width: 75, height: 75)
43 |
44 | VStack(alignment: .leading) {
45 | Text(try! AttributedString(markdown: "**\(member.nickname.transformingHalfwidthFullwidth())** \(activity.activity.transformingHalfwidthFullwidth())"))
46 | .lineLimit(2)
47 | Spacer()
48 | HStack {
49 | Spacer()
50 | Text(DateTool.ymdhmsDateString(from: TimeInterval(activity.timestamp)))
51 | .font(.caption)
52 | }
53 | }
54 | .frame(maxWidth: .infinity, maxHeight: .infinity)
55 | }
56 | .frame(height: 75)
57 | .padding(.horizontal)
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/chafenqi/View/v2/Team/TeamLandingPage.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TeamLandingPage.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2024/12/24.
6 | //
7 |
8 | import Foundation
9 | import SwiftUI
10 |
11 | struct TeamLandingPage: View {
12 | @ObservedObject var team: CFQTeam
13 | @ObservedObject var user: CFQNUser
14 |
15 | var body: some View {
16 | VStack {
17 | if team.isLoading {
18 | ProgressView() {
19 | Text("加载中...")
20 | }
21 | } else {
22 | if team.currentTeamId != nil, team.current.info.displayName != "" {
23 | TeamInfoPage(team: team, user: user)
24 | } else {
25 | TeamIntroductionPage(team: team, user: user)
26 | }
27 | }
28 | }
29 | .onAppear {
30 | team.refresh(user: user)
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/chafenqi/View/v2/Updater/UpdaterRootView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UpdaterRootView.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2023/5/8.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct UpdaterRootView: View {
11 | @ObservedObject var user: CFQNUser
12 | @ObservedObject var service = TunnelManagerService.shared
13 |
14 |
15 | var body: some View {
16 | VStack {
17 | if (service.manager != nil) {
18 | UpdaterView(user: user)
19 | } else {
20 | UpdaterWelcomeView()
21 | // UpdaterQRCodeView(maiStr: "afbfdahfdaytjhfdnvfsnuafbfdahfdaytjhfdnvfsnuafbfdahfdaytjhfdnvfsnu", chuStr: "afbfdahfdaytjhfdnvfsnutgrsatdvatersadafbfdahfdaytjhfdnvfsnuafbfdahfdaytjhfdnvfsnu")
22 | }
23 | }
24 | .onAppear {
25 | loadProfileFromDevice()
26 | }
27 | .navigationTitle("传分")
28 | .navigationBarTitleDisplayMode(.inline)
29 |
30 | }
31 |
32 | func loadProfileFromDevice() {
33 | service.loadProfile { result in
34 | switch result {
35 | case .success():
36 | print("[Updater] Profile Loaded.")
37 | case .failure(let error):
38 | print(error)
39 | }
40 | }
41 | }
42 | }
43 |
44 | struct UpdaterRootView_Previews: PreviewProvider {
45 | static var previews: some View {
46 | UpdaterRootView(user: CFQNUser())
47 | }
48 | }
49 |
50 |
51 |
--------------------------------------------------------------------------------
/chafenqi/View/v2/Updater/UpdaterWelcomeView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UpdaterWelcomeView.swift
3 | // chafenqi
4 | //
5 | // Created by 刘易斯 on 2023/2/8.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct UpdaterWelcomeView: View {
11 | @ObservedObject var service = TunnelManagerService.shared
12 |
13 | @State var isInstalling = false
14 |
15 | var body: some View {
16 | VStack {
17 | Text("首次使用请安装VPN描述文件")
18 | .font(.title2)
19 | .bold()
20 | .padding()
21 |
22 | if(isInstalling) {
23 | ProgressView()
24 | } else {
25 | Button {
26 | installProfileToDevice()
27 | } label: {
28 | Text("安装")
29 | }
30 | .buttonStyle(.automatic)
31 | }
32 | }
33 | }
34 |
35 | func installProfileToDevice() {
36 | isInstalling = true
37 |
38 | service.installProfile { result in
39 | switch result {
40 | case .success():
41 | isInstalling = false
42 | case .failure(let error):
43 | print(error)
44 | isInstalling = false
45 | }
46 | }
47 | }
48 | }
49 |
50 | struct UpdaterWelcomeView_Previews: PreviewProvider {
51 | static var previews: some View {
52 | UpdaterWelcomeView()
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/chafenqi/chafenqi.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | aps-environment
6 | development
7 | com.apple.developer.networking.networkextension
8 |
9 | packet-tunnel-provider
10 |
11 | com.apple.developer.siri
12 |
13 | com.apple.security.application-groups
14 |
15 | group.com.nltv.chafenqi.shared
16 | group.com.nltv.chafenqi.onesignal
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/chafenqiMini/BuildURLHandler.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BuildURLHandler.swift
3 | // chafenqiMini
4 | //
5 | // Created by 刘易斯 on 2023/4/18.
6 | //
7 |
8 | import Intents
9 |
10 | class BuildURLHandler: NSObject, BuildProxyURLIntentHandling {
11 | func resolveForward(for intent: BuildProxyURLIntent, with completion: @escaping (INBooleanResolutionResult) -> Void) {
12 | guard let forward = intent.forward else {
13 | completion(INBooleanResolutionResult.needsValue())
14 | return
15 | }
16 | completion(INBooleanResolutionResult.success(with: forward as! Bool))
17 | }
18 |
19 | func handle(intent: BuildProxyURLIntent, completion: @escaping (BuildProxyURLIntentResponse) -> Void) {
20 | if let token = intent.token {
21 | var uploadQuery = ""
22 | let destination = intent.destination
23 | switch destination {
24 | case .unknown:
25 | fatalError("Cannot select unknown")
26 | case .chunithm:
27 | uploadQuery = "upload/chunithm"
28 | case .maimai:
29 | uploadQuery = "upload/maimai"
30 | }
31 | let forward = intent.forward
32 | let response = BuildProxyURLIntentResponse(code: .success, userActivity: nil)
33 | let url = "http://\(SharedValues.serverAddress):\(SharedValues.proxyServerPort)/\(uploadQuery)?jwt=\(token)"
34 | response.url = url
35 | completion(response)
36 | }
37 | }
38 |
39 | func resolveToken(for intent: BuildProxyURLIntent, with completion: @escaping (INStringResolutionResult) -> Void) {
40 | guard let token = intent.token else {
41 | completion(INStringResolutionResult.needsValue())
42 | return
43 | }
44 | completion(INStringResolutionResult.success(with: token))
45 | }
46 |
47 | func resolveDestination(for intent: BuildProxyURLIntent, with completion: @escaping (DestinationsResolutionResult) -> Void) {
48 | switch intent.destination {
49 | case .maimai, .chunithm:
50 | completion(DestinationsResolutionResult.success(with: intent.destination))
51 | case .unknown:
52 | completion(DestinationsResolutionResult.needsValue())
53 | return
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/chafenqiMini/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | NSExtension
6 |
7 | NSExtensionAttributes
8 |
9 | IntentsRestrictedWhileLocked
10 |
11 | IntentsRestrictedWhileProtectedDataUnavailable
12 |
13 | IntentsSupported
14 |
15 | BuildProxyURLIntent
16 | FetchFishTokenIntent
17 | StartProxyIntent
18 | StopProxyIntent
19 |
20 |
21 | NSExtensionPointIdentifier
22 | com.apple.intents-service
23 | NSExtensionPrincipalClass
24 | $(PRODUCT_MODULE_NAME).IntentHandler
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/chafenqiMini/IntentHandler.swift:
--------------------------------------------------------------------------------
1 | //
2 | // IntentHandler.swift
3 | // chafenqiMini
4 | //
5 | // Created by 刘易斯 on 2023/4/18.
6 | //
7 |
8 | import Intents
9 |
10 | class IntentHandler: INExtension, StartProxyIntentHandling, StopProxyIntentHandling, FetchFishTokenIntentHandling {
11 |
12 | func handle(intent: StartProxyIntent, completion: @escaping (StartProxyIntentResponse) -> Void) {
13 | NSLog("Start Proxy")
14 |
15 | let response = StartProxyIntentResponse(code: .continueInApp, userActivity: NSUserActivity(activityType: "StartProxyIntent"))
16 | completion(response)
17 | }
18 |
19 | func handle(intent: StopProxyIntent, completion: @escaping (StopProxyIntentResponse) -> Void) {
20 | NSLog("Stop Proxy")
21 | let response = StopProxyIntentResponse(code: .continueInApp, userActivity: NSUserActivity(activityType: "StopProxyIntent"))
22 | completion(response)
23 | }
24 |
25 | func handle(intent: FetchFishTokenIntent) async -> FetchFishTokenIntentResponse {
26 | let savedToken = UserDefaults(suiteName: "group.com.nltv.chafenqi.shared")!.string(forKey: "JWT")
27 | guard let token = savedToken else {
28 | NSLog("Failed to retreive from user defaults")
29 | return FetchFishTokenIntentResponse(code: .failure, userActivity: nil)
30 | }
31 | let response = FetchFishTokenIntentResponse(code: .success, userActivity: nil)
32 | response.token = token
33 | return response
34 | }
35 |
36 | override func handler(for intent: INIntent) -> Any {
37 | // This is the default implementation. If you want different objects to handle different intents,
38 | // you can override this and return the handler you want for that particular intent.
39 | if intent is BuildProxyURLIntent {
40 | return BuildURLHandler()
41 | } else {
42 | return self
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/chafenqiMini/chafenqiMini.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.developer.networking.networkextension
6 |
7 | packet-tunnel-provider
8 |
9 | com.apple.security.application-groups
10 |
11 | group.com.nltv.chafenqi.shared
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/chafenqiNotifier/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | NSExtension
6 |
7 | NSExtensionPointIdentifier
8 | com.apple.usernotifications.service
9 | NSExtensionPrincipalClass
10 | $(PRODUCT_MODULE_NAME).NotificationService
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/chafenqiNotifier/NotificationService.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NotificationService.swift
3 | // chafenqiNotifier
4 | //
5 | // Created by 刘易斯 on 2023/6/10.
6 | //
7 |
8 | import UserNotifications
9 |
10 | import OneSignalExtension
11 |
12 | class NotificationService: UNNotificationServiceExtension {
13 |
14 | var contentHandler: ((UNNotificationContent) -> Void)?
15 | var receivedRequest: UNNotificationRequest!
16 | var bestAttemptContent: UNMutableNotificationContent?
17 |
18 | override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
19 | self.receivedRequest = request
20 | self.contentHandler = contentHandler
21 | self.bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
22 |
23 | if let bestAttemptContent = bestAttemptContent {
24 | OneSignalExtension.didReceiveNotificationExtensionRequest(self.receivedRequest, with: bestAttemptContent, withContentHandler: self.contentHandler)
25 | }
26 | }
27 |
28 | override func serviceExtensionTimeWillExpire() {
29 | // Called just before the extension will be terminated by the system.
30 | // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
31 | if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent {
32 | OneSignalExtension.serviceExtensionTimeWillExpireRequest(self.receivedRequest, with: self.bestAttemptContent)
33 | contentHandler(bestAttemptContent)
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/chafenqiNotifier/chafenqiNotifier.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.application-groups
6 |
7 | group.com.nltv.chafenqi.onesignal
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/chafenqiTests/chafenqiTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // chafenqiTests.swift
3 | // chafenqiTests
4 | //
5 | // Created by 刘易斯 on 2023/1/6.
6 | //
7 |
8 | import XCTest
9 | @testable import chafenqi
10 |
11 | final class chafenqiTests: XCTestCase {
12 |
13 | override func setUpWithError() throws {
14 | // Put setup code here. This method is called before the invocation of each test method in the class.
15 | }
16 |
17 | override func tearDownWithError() throws {
18 | // Put teardown code here. This method is called after the invocation of each test method in the class.
19 | }
20 |
21 | func testExample() throws {
22 | // This is an example of a functional test case.
23 | // Use XCTAssert and related functions to verify your tests produce the correct results.
24 | // Any test you write for XCTest can be annotated as throws and async.
25 | // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error.
26 | // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards.
27 | }
28 |
29 | func testPerformanceExample() throws {
30 | // This is an example of a performance test case.
31 | self.measure {
32 | // Put the code you want to measure the time of here.
33 | }
34 | }
35 |
36 | }
37 |
--------------------------------------------------------------------------------
/chafenqiUITests/chafenqiUITests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // chafenqiUITests.swift
3 | // chafenqiUITests
4 | //
5 | // Created by 刘易斯 on 2023/1/6.
6 | //
7 |
8 | import XCTest
9 |
10 | final class chafenqiUITests: XCTestCase {
11 |
12 | override func setUpWithError() throws {
13 | // Put setup code here. This method is called before the invocation of each test method in the class.
14 |
15 | // In UI tests it is usually best to stop immediately when a failure occurs.
16 | continueAfterFailure = false
17 |
18 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
19 | }
20 |
21 | override func tearDownWithError() throws {
22 | // Put teardown code here. This method is called after the invocation of each test method in the class.
23 | }
24 |
25 | func testExample() throws {
26 | // UI tests must launch the application that they test.
27 | let app = XCUIApplication()
28 | app.launch()
29 |
30 | // Use XCTAssert and related functions to verify your tests produce the correct results.
31 | }
32 |
33 | func testLaunchPerformance() throws {
34 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) {
35 | // This measures how long it takes to launch your application.
36 | measure(metrics: [XCTApplicationLaunchMetric()]) {
37 | XCUIApplication().launch()
38 | }
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/chafenqiUITests/chafenqiUITestsLaunchTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // chafenqiUITestsLaunchTests.swift
3 | // chafenqiUITests
4 | //
5 | // Created by 刘易斯 on 2023/1/6.
6 | //
7 |
8 | import XCTest
9 |
10 | final class chafenqiUITestsLaunchTests: XCTestCase {
11 |
12 | override class var runsForEachTargetApplicationUIConfiguration: Bool {
13 | true
14 | }
15 |
16 | override func setUpWithError() throws {
17 | continueAfterFailure = false
18 | }
19 |
20 | func testLaunch() throws {
21 | let app = XCUIApplication()
22 | app.launch()
23 |
24 | // Insert steps here to perform after app launch but before taking a screenshot,
25 | // such as logging into a test account or navigating somewhere in the app
26 |
27 | let attachment = XCTAttachment(screenshot: app.screenshot())
28 | attachment.name = "Launch Screen"
29 | attachment.lifetime = .keepAlways
30 | add(attachment)
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/infoWidget/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/infoWidget/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "platform" : "ios",
6 | "size" : "1024x1024"
7 | }
8 | ],
9 | "info" : {
10 | "author" : "xcode",
11 | "version" : 1
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/infoWidget/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/infoWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/infoWidget/Assets.xcassets/penguin.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "nameplate_penguin.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/infoWidget/Assets.xcassets/penguin.imageset/nameplate_penguin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Louiswu2011/chafenqi/206cb81997741633392f743c6c06d7ec754e7e13/infoWidget/Assets.xcassets/penguin.imageset/nameplate_penguin.png
--------------------------------------------------------------------------------
/infoWidget/Assets.xcassets/salt.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "nameplate_salt.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/infoWidget/Assets.xcassets/salt.imageset/nameplate_salt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Louiswu2011/chafenqi/206cb81997741633392f743c6c06d7ec754e7e13/infoWidget/Assets.xcassets/salt.imageset/nameplate_salt.png
--------------------------------------------------------------------------------
/infoWidget/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | NSExtension
6 |
7 | NSExtensionPointIdentifier
8 | com.apple.widgetkit-extension
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/infoWidget/infoWidgetBundle.swift:
--------------------------------------------------------------------------------
1 | //
2 | // infoWidgetBundle.swift
3 | // infoWidget
4 | //
5 | // Created by xinyue on 2023/6/27.
6 | //
7 |
8 | import WidgetKit
9 | import SwiftUI
10 |
11 | @main
12 | struct infoWidgetBundle: WidgetBundle {
13 | var body: some Widget {
14 | infoWidget()
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/infoWidgetExtension.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.application-groups
6 |
7 | group.com.nltv.chafenqi.shared
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # 中二节奏/舞萌DX查分器(暂定)
2 | **QQ交流群号:706609485**
3 |
4 | 用Swift实现的轻量级查分器,支持iOS14及以上。
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | ## 主要功能
13 | - [x] 基本信息查询
14 | - [x] Rating/B30/R10计算
15 | - [x] 搜索曲目
16 | - [x] App内查看谱面(仅Expert及以上)
17 | - [x] 个人最佳成绩查询
18 | - [x] 分数上传
19 | ## 使用方法
20 | ### TestFlight(推荐)
21 | 请到Q群内获取邀请链接!
22 | ### 自签名(更新较慢,不推荐)
23 | 1. 下载Release中的ipa文件
24 | 2. 用Altstore或其他自签方式安装
25 | 3. 用Diving-Fish查分器账号登录
26 | 4. 开始查分!
27 | ## 注意事项
28 | - 偶发闪退
29 | - 暂不支持中二WE谱面
30 | ## 编译项目
31 | 1. Clone到本地
32 | 2. 用Xcode打开编译
33 | ## 特别感谢
34 | - [@SoreHait](https://github.com/SoreHait)
35 | - [@Diving-Fish的查分器](https://github.com/Diving-Fish/maimaidx-prober)
36 | - [@bakapiano的国服代理更新方案](https://github.com/bakapiano/maimaidx-prober-proxy-updater)
37 | - [sdvx.in的谱面预览](https://sdvx.in)
38 |
--------------------------------------------------------------------------------
/updater/ChafenqiTunnelProvider.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ChafenqiTunnelProvider.swift
3 | // updater
4 | //
5 | // Created by 刘易斯 on 2023/12/2.
6 | //
7 |
8 | import Foundation
9 |
--------------------------------------------------------------------------------
/updater/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | NSExtension
6 |
7 | NSExtensionPointIdentifier
8 | com.apple.networkextension.packet-tunnel
9 | NSExtensionPrincipalClass
10 | $(PRODUCT_MODULE_NAME).UpdaterTunnelProvider
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/updater/LocalServer/ChunithmNetHandler.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ChunithmNetHandler.swift
3 | // updater
4 | //
5 | // Created by 刘易斯 on 2023/2/7.
6 | //
7 |
8 | import Foundation
9 | import NIO
10 | import NIOHTTP1
11 |
12 | final class ChunithmNetHandler {
13 |
14 |
15 | }
16 |
17 | extension ChunithmNetHandler: ChannelOutboundHandler {
18 | typealias OutboundIn = HTTPClientRequestPart
19 | typealias OutboundOut = HTTPClientRequestPart
20 |
21 | func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise?) {
22 | guard case .head(var head) = self.unwrapOutboundIn(data) else {
23 | context.write(data, promise: promise)
24 | return
25 | }
26 |
27 | NSLog(head.uri)
28 | context.write(data, promise: promise)
29 | }
30 | }
31 |
32 |
33 |
--------------------------------------------------------------------------------
/updater/LocalServer/GlueHandler.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GlueHandler.swift
3 | // updater
4 | //
5 | // Created by 刘易斯 on 2023/2/8.
6 | //
7 |
8 | import NIOCore
9 |
10 | final class GlueHandler {
11 |
12 | private var partner: GlueHandler?
13 |
14 | private var context: ChannelHandlerContext?
15 |
16 | private var pendingRead: Bool = false
17 |
18 | private init() { }
19 | }
20 |
21 |
22 | extension GlueHandler {
23 | static func matchedPair() -> (GlueHandler, GlueHandler) {
24 | let first = GlueHandler()
25 | let second = GlueHandler()
26 |
27 | first.partner = second
28 | second.partner = first
29 |
30 | return (first, second)
31 | }
32 | }
33 |
34 |
35 | extension GlueHandler {
36 | private func partnerWrite(_ data: NIOAny) {
37 | self.context?.write(data, promise: nil)
38 | }
39 |
40 | private func partnerFlush() {
41 | self.context?.flush()
42 | }
43 |
44 | private func partnerWriteEOF() {
45 | self.context?.close(mode: .output, promise: nil)
46 | }
47 |
48 | private func partnerCloseFull() {
49 | self.context?.close(promise: nil)
50 | }
51 |
52 | private func partnerBecameWritable() {
53 | if self.pendingRead {
54 | self.pendingRead = false
55 | self.context?.read()
56 | }
57 | }
58 |
59 | private var partnerWritable: Bool {
60 | self.context?.channel.isWritable ?? false
61 | }
62 | }
63 |
64 |
65 | extension GlueHandler: ChannelDuplexHandler {
66 | typealias InboundIn = NIOAny
67 | typealias OutboundIn = NIOAny
68 | typealias OutboundOut = NIOAny
69 |
70 | func handlerAdded(context: ChannelHandlerContext) {
71 | self.context = context
72 | }
73 |
74 | func handlerRemoved(context: ChannelHandlerContext) {
75 | self.context = nil
76 | self.partner = nil
77 | }
78 |
79 | func channelRead(context: ChannelHandlerContext, data: NIOAny) {
80 | self.partner?.partnerWrite(data)
81 | }
82 |
83 | func channelReadComplete(context: ChannelHandlerContext) {
84 | self.partner?.partnerFlush()
85 | }
86 |
87 | func channelInactive(context: ChannelHandlerContext) {
88 | self.partner?.partnerCloseFull()
89 | }
90 |
91 | func userInboundEventTriggered(context: ChannelHandlerContext, event: Any) {
92 | if let event = event as? ChannelEvent, case .inputClosed = event {
93 | // We have read EOF.
94 | self.partner?.partnerWriteEOF()
95 | }
96 | }
97 |
98 | func errorCaught(context: ChannelHandlerContext, error: Error) {
99 | self.partner?.partnerCloseFull()
100 | }
101 |
102 | func channelWritabilityChanged(context: ChannelHandlerContext) {
103 | if context.channel.isWritable {
104 | self.partner?.partnerBecameWritable()
105 | }
106 | }
107 |
108 | func read(context: ChannelHandlerContext) {
109 | if let partner = self.partner, partner.partnerWritable {
110 | context.read()
111 | } else {
112 | self.pendingRead = true
113 | }
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/updater/LocalServer/ProxyServer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ProxyServer.swift
3 | // updater
4 | //
5 | // Created by 刘易斯 on 2023/2/7.
6 | //
7 |
8 | import Foundation
9 | import NIO
10 | import NIOTransportServices
11 | import NIOHTTP1
12 |
13 | class ProxyServer {
14 | let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
15 |
16 | init(host: String, port: Int) {
17 | self.host = host
18 | self.port = port
19 | }
20 |
21 | func start(_ completion: @escaping (Result) -> Void) {
22 |
23 | let bootstrap = ServerBootstrap(group: group)
24 | .serverChannelOption(ChannelOptions.socket(SOL_SOCKET, SO_REUSEADDR), value: 1)
25 | .childChannelOption(ChannelOptions.socket(SOL_SOCKET, SO_REUSEADDR), value: 1)
26 | .childChannelInitializer { channel in
27 | channel.pipeline.addHandler(ByteToMessageHandler(HTTPRequestDecoder(leftOverBytesStrategy: .forwardBytes)))
28 | .flatMap { channel.pipeline.addHandler(HTTPResponseEncoder()) }
29 | .flatMap { channel.pipeline.addHandler(ConnectHandler()) }
30 | }
31 |
32 | bootstrap.bind(to: try! SocketAddress(ipAddress: host, port: port)).whenComplete { result in
33 | // Need to create this here for thread-safety purposes
34 | switch result {
35 | case .success(let channel):
36 | NSLog("Listening on \(String(describing: channel.localAddress))")
37 | completion(.success(()))
38 | case .failure(let error):
39 | NSLog("Failed to bind 127.0.0.1:8080, \(error)")
40 | completion(.failure(error))
41 | }
42 | }
43 | }
44 |
45 | func stop() {
46 | do {
47 | try group.syncShutdownGracefully()
48 | NSLog("Local server shutdown.")
49 | } catch {
50 |
51 | }
52 |
53 | }
54 |
55 | private var host: String
56 | private var port: Int
57 | }
58 |
--------------------------------------------------------------------------------
/updater/Server/ProxyServer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // main.swift
3 | // updater
4 | //
5 | // Created by 刘易斯 on 2023/2/7.
6 | //
7 |
8 | import Dispatch
9 | import NIOPosix
10 |
11 |
12 | class ProxyServer {
13 | func start() {
14 | let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
15 |
16 | let bootstrap = ServerBootstrap(group: group)
17 |
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/updater/updater.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.developer.networking.networkextension
6 |
7 | packet-tunnel-provider
8 |
9 | com.apple.security.application-groups
10 |
11 | group.com.nltv.chafenqi.shared
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------