├── .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 | --------------------------------------------------------------------------------