├── .gitignore ├── .periphery.yml ├── EMURoutingTracker.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved └── xcshareddata │ └── xcschemes │ └── EMURoutingTracker.xcscheme ├── EMURoutingTracker ├── AppDelegate.swift ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── 100.png │ │ ├── 1024.png │ │ ├── 114.png │ │ ├── 120.png │ │ ├── 144.png │ │ ├── 152.png │ │ ├── 167.png │ │ ├── 172.png │ │ ├── 180.png │ │ ├── 196.png │ │ ├── 20.png │ │ ├── 216.png │ │ ├── 29.png │ │ ├── 40.png │ │ ├── 48.png │ │ ├── 50.png │ │ ├── 55.png │ │ ├── 57.png │ │ ├── 58.png │ │ ├── 60.png │ │ ├── 72.png │ │ ├── 76.png │ │ ├── 80.png │ │ ├── 87.png │ │ ├── 88.png │ │ ├── Contents.json │ │ ├── Untitled-2.png │ │ ├── Untitled-2128.png │ │ ├── Untitled-2128@0.25x-1.png │ │ ├── Untitled-2128@0.25x-2.png │ │ ├── Untitled-2128@0.25x.png │ │ ├── Untitled-2128@0.5x.png │ │ ├── Untitled-2@0.25x-1.png │ │ ├── Untitled-2@0.25x.png │ │ ├── Untitled-2@0.5x-1.png │ │ └── Untitled-2@0.5x.png │ ├── CR200J.imageset │ │ ├── Contents.json │ │ └── cn-cr200j.png │ ├── CR300A.imageset │ │ ├── Contents.json │ │ └── cn-cr300af.png │ ├── CR300B.imageset │ │ ├── Contents.json │ │ └── cn-cr300bf.png │ ├── CR400A.imageset │ │ ├── Contents.json │ │ └── cn-cr400a.png │ ├── CR400B.imageset │ │ ├── Contents.json │ │ └── cn-cr400b.png │ ├── CRH1.imageset │ │ ├── Contents.json │ │ └── cn-crh1.png │ ├── CRH1E.imageset │ │ ├── Contents.json │ │ └── cn-crh1e.png │ ├── CRH2.imageset │ │ ├── Contents.json │ │ └── cn-crh2.png │ ├── CRH2B.imageset │ │ ├── Contents.json │ │ └── cn-crh2b.png │ ├── CRH2C.imageset │ │ ├── Contents.json │ │ └── cn-crh2c.png │ ├── CRH2H.imageset │ │ ├── Contents.json │ │ └── cn-crh2h.png │ ├── CRH3.imageset │ │ ├── Contents.json │ │ └── cn-crh3.png │ ├── CRH380.imageset │ │ ├── Contents.json │ │ └── cn-crh380.png │ ├── CRH380B.imageset │ │ ├── Contents.json │ │ └── cn-crh380b.png │ ├── CRH380C.imageset │ │ ├── Contents.json │ │ └── cn-crh380c.png │ ├── CRH380D.imageset │ │ ├── Contents.json │ │ └── cn-crh380d.png │ ├── CRH3A.imageset │ │ ├── Contents.json │ │ └── cn-crh3a.png │ ├── CRH3G.imageset │ │ ├── Contents.json │ │ └── cn-crh3g.png │ ├── CRH5.imageset │ │ ├── Contents.json │ │ └── cn-crh5.png │ ├── CRH6.imageset │ │ ├── Contents.json │ │ └── cn-crh6.png │ ├── CRH6F.imageset │ │ ├── Contents.json │ │ └── cn-crh6f.png │ ├── Contents.json │ └── MTR.imageset │ │ ├── Contents.json │ │ └── hk-mtr380a.png ├── ContentView.swift ├── EMURoutingTracker.entitlements ├── EMURoutingTracker.swift ├── Info.plist ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── Util │ └── Query.swift ├── View │ ├── Button │ │ ├── FavoriteButton.swift │ │ └── ScanQRCodeButton.swift │ ├── List │ │ ├── DepartureArrivalList.swift │ │ ├── MultipleEMUList.swift │ │ ├── SingleEMUList.swift │ │ ├── TrainList.swift │ │ └── TrainOrEMUView.swift │ ├── Picker │ │ └── StationPicker.swift │ ├── Row │ │ ├── DepartureArrivalRow.swift │ │ ├── EMUAndTrainRow.swift │ │ ├── EMURow.swift │ │ ├── EmptyRow.swift │ │ └── TrainRow.swift │ └── Tab │ │ ├── AboutView.swift │ │ ├── FavoritesView.swift │ │ └── QueryView.swift └── ViewModifier │ ├── QueryNavigationModifier.swift │ └── ScanQRCodeActionSheetModifier.swift ├── EMURoutingTrackerFramework ├── EMURoutingTrackerFramework.h ├── Info.plist ├── Model │ ├── CRResponse.swift │ ├── DepartureArrival.swift │ ├── EMUTrainAssociation.swift │ ├── Station.swift │ └── Train.swift ├── Provider │ ├── AbstractProvider.swift │ ├── FavoritesProvider.swift │ ├── StationProvider.swift │ └── TrainInfoProvider.swift ├── Request │ ├── CRRequest.swift │ └── MoerailRequest.swift ├── Util │ ├── DateFormatter.swift │ └── UserDefaults.swift └── ViewModel │ ├── DepartureArrivalViewModel.swift │ ├── EMUTrainViewModel.swift │ └── FavoritesViewModel.swift ├── EMURoutingTrackerWidget ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── CR200J.imageset │ │ ├── Contents.json │ │ ├── cn-cr200j-1.png │ │ ├── cn-cr200j-2.png │ │ └── cn-cr200j.png │ ├── CR300A.imageset │ │ ├── Contents.json │ │ ├── cn-cr300af-1.png │ │ ├── cn-cr300af-2.png │ │ └── cn-cr300af.png │ ├── CR300B.imageset │ │ ├── Contents.json │ │ ├── cn-cr300bf-1.png │ │ ├── cn-cr300bf-2.png │ │ └── cn-cr300bf.png │ ├── CR400A.imageset │ │ ├── Contents.json │ │ ├── cn-cr400a-1.png │ │ ├── cn-cr400a-2.png │ │ └── cn-cr400a.png │ ├── CR400B.imageset │ │ ├── Contents.json │ │ ├── cn-cr400b-1.png │ │ ├── cn-cr400b-2.png │ │ └── cn-cr400b.png │ ├── CRH1.imageset │ │ ├── Contents.json │ │ ├── cn-crh1-1.png │ │ ├── cn-crh1-2.png │ │ └── cn-crh1.png │ ├── CRH1E.imageset │ │ ├── Contents.json │ │ ├── cn-crh1e-1.png │ │ ├── cn-crh1e-2.png │ │ └── cn-crh1e.png │ ├── CRH2.imageset │ │ ├── Contents.json │ │ ├── cn-crh2-1.png │ │ ├── cn-crh2-2.png │ │ └── cn-crh2.png │ ├── CRH2B.imageset │ │ ├── Contents.json │ │ ├── cn-crh2b-1.png │ │ ├── cn-crh2b-2.png │ │ └── cn-crh2b.png │ ├── CRH2C.imageset │ │ ├── Contents.json │ │ ├── cn-crh2c-1.png │ │ ├── cn-crh2c-2.png │ │ └── cn-crh2c.png │ ├── CRH2H.imageset │ │ ├── Contents.json │ │ ├── cn-crh2h-1.png │ │ ├── cn-crh2h-2.png │ │ └── cn-crh2h.png │ ├── CRH3.imageset │ │ ├── Contents.json │ │ ├── cn-crh3-1.png │ │ ├── cn-crh3-2.png │ │ └── cn-crh3.png │ ├── CRH380.imageset │ │ ├── Contents.json │ │ ├── cn-crh380-1.png │ │ ├── cn-crh380-2.png │ │ └── cn-crh380.png │ ├── CRH380B.imageset │ │ ├── Contents.json │ │ ├── cn-crh380b-1.png │ │ ├── cn-crh380b-2.png │ │ └── cn-crh380b.png │ ├── CRH380C.imageset │ │ ├── Contents.json │ │ ├── cn-crh380c-1.png │ │ ├── cn-crh380c-2.png │ │ └── cn-crh380c.png │ ├── CRH380D.imageset │ │ ├── Contents.json │ │ ├── cn-crh380d-1.png │ │ ├── cn-crh380d-2.png │ │ └── cn-crh380d.png │ ├── CRH3A.imageset │ │ ├── Contents.json │ │ ├── cn-crh3a-1.png │ │ ├── cn-crh3a-2.png │ │ └── cn-crh3a.png │ ├── CRH3G.imageset │ │ ├── Contents.json │ │ ├── cn-crh3g-1.png │ │ ├── cn-crh3g-2.png │ │ └── cn-crh3g.png │ ├── CRH5.imageset │ │ ├── Contents.json │ │ ├── cn-crh5-1.png │ │ ├── cn-crh5-2.png │ │ └── cn-crh5.png │ ├── CRH6.imageset │ │ ├── Contents.json │ │ ├── cn-crh6-1.png │ │ ├── cn-crh6-2.png │ │ └── cn-crh6.png │ ├── CRH6F.imageset │ │ ├── Contents.json │ │ ├── cn-crh6f-1.png │ │ ├── cn-crh6f-2.png │ │ └── cn-crh6f.png │ ├── Contents.json │ ├── MTR.imageset │ │ ├── Contents.json │ │ ├── hk-mtr380a-1.png │ │ ├── hk-mtr380a-2.png │ │ └── hk-mtr380a.png │ └── WidgetBackground.colorset │ │ └── Contents.json ├── ChinaEMUWidget.swift └── Info.plist ├── EMURoutingTrackerWidgetExtension.entitlements ├── README.md └── ci_scripts └── ci_post_xcodebuild.sh /.gitignore: -------------------------------------------------------------------------------- 1 | Pods 2 | # Xcode 3 | # 4 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 5 | 6 | ## User settings 7 | xcuserdata/ 8 | 9 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 10 | *.xcscmblueprint 11 | *.xccheckout 12 | 13 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 14 | build/ 15 | DerivedData/ 16 | *.moved-aside 17 | *.pbxuser 18 | !default.pbxuser 19 | *.mode1v3 20 | !default.mode1v3 21 | *.mode2v3 22 | !default.mode2v3 23 | *.perspectivev3 24 | !default.perspectivev3 25 | 26 | ## Gcc Patch 27 | /*.gcno 28 | .DS_Store 29 | -------------------------------------------------------------------------------- /.periphery.yml: -------------------------------------------------------------------------------- 1 | project: EMURoutingTracker.xcodeproj 2 | schemes: 3 | - EMURoutingTracker 4 | - EMURoutingTrackerFramework 5 | - EMURoutingTrackerWidgetExtension 6 | targets: 7 | - EMURoutingTracker 8 | - EMURoutingTrackerFramework 9 | - EMURoutingTrackerWidgetExtension 10 | -------------------------------------------------------------------------------- /EMURoutingTracker.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /EMURoutingTracker.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /EMURoutingTracker.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "47af5e1d5b916b68bd82afe6722e15f17dc7a85f3fb286f050fb1e7b625e69b4", 3 | "pins" : [ 4 | { 5 | "identity" : "alamofire", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/Alamofire/Alamofire.git", 8 | "state" : { 9 | "revision" : "513364f870f6bfc468f9d2ff0a95caccc10044c5", 10 | "version" : "5.10.2" 11 | } 12 | }, 13 | { 14 | "identity" : "cache", 15 | "kind" : "remoteSourceControl", 16 | "location" : "https://github.com/hyperoslo/Cache", 17 | "state" : { 18 | "revision" : "24e47109e31b2031cb26e25cc1b81b607496066c", 19 | "version" : "7.4.0" 20 | } 21 | }, 22 | { 23 | "identity" : "codescanner", 24 | "kind" : "remoteSourceControl", 25 | "location" : "https://github.com/twostraws/CodeScanner", 26 | "state" : { 27 | "revision" : "5e886430238944c7200fc9e10dbf2d9550dba865", 28 | "version" : "2.5.2" 29 | } 30 | }, 31 | { 32 | "identity" : "moya", 33 | "kind" : "remoteSourceControl", 34 | "location" : "https://github.com/Moya/Moya.git", 35 | "state" : { 36 | "revision" : "c263811c1f3dbf002be9bd83107f7cdc38992b26", 37 | "version" : "15.0.3" 38 | } 39 | }, 40 | { 41 | "identity" : "reactiveswift", 42 | "kind" : "remoteSourceControl", 43 | "location" : "https://github.com/ReactiveCocoa/ReactiveSwift.git", 44 | "state" : { 45 | "revision" : "c43bae3dac73fdd3cb906bd5a1914686ca71ed3c", 46 | "version" : "6.7.0" 47 | } 48 | }, 49 | { 50 | "identity" : "rxswift", 51 | "kind" : "remoteSourceControl", 52 | "location" : "https://github.com/ReactiveX/RxSwift.git", 53 | "state" : { 54 | "revision" : "c7c7d2cf50a3211fe2843f76869c698e4e417930", 55 | "version" : "6.8.0" 56 | } 57 | }, 58 | { 59 | "identity" : "sentry-cocoa", 60 | "kind" : "remoteSourceControl", 61 | "location" : "https://github.com/getsentry/sentry-cocoa", 62 | "state" : { 63 | "revision" : "56bfb7e723c76614be4c0861ee820ccbaed14c6d", 64 | "version" : "8.41.0" 65 | } 66 | }, 67 | { 68 | "identity" : "sfsafesymbols", 69 | "kind" : "remoteSourceControl", 70 | "location" : "https://github.com/SFSafeSymbols/SFSafeSymbols.git", 71 | "state" : { 72 | "revision" : "e2e28f4e56e1769c2ec3c61c9355fc64eb7a535a", 73 | "version" : "5.3.0" 74 | } 75 | }, 76 | { 77 | "identity" : "swiftyuserdefaults", 78 | "kind" : "remoteSourceControl", 79 | "location" : "https://github.com/sunshinejr/SwiftyUserDefaults", 80 | "state" : { 81 | "revision" : "f66bcd04088582c8fbb5cb8554d577e303bae396", 82 | "version" : "5.3.0" 83 | } 84 | } 85 | ], 86 | "version" : 3 87 | } 88 | -------------------------------------------------------------------------------- /EMURoutingTracker.xcodeproj/xcshareddata/xcschemes/EMURoutingTracker.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 55 | 61 | 62 | 63 | 64 | 70 | 72 | 78 | 79 | 80 | 81 | 83 | 84 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /EMURoutingTracker/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // EMURoutingTracker 4 | // 5 | // Created by Qingyang Hu on 11/10/20. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | import BackgroundTasks 11 | import Sentry 12 | 13 | class AppDelegate: NSObject, UIApplicationDelegate { 14 | 15 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { 16 | UserDefaultsMigrater.migrate() 17 | SentrySDK.start { options in 18 | options.dsn = "https://85987290d32948b7a5434c6604a8d283@sentry.io/1545955" 19 | } 20 | return true 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | }, 6 | { 7 | "appearances" : [ 8 | { 9 | "appearance" : "luminosity", 10 | "value" : "dark" 11 | } 12 | ], 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/AppIcon.appiconset/100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqy2000/EMURoutingTracker/0a53f38f8a72c08ab9be93715e66be1d45148a1a/EMURoutingTracker/Assets.xcassets/AppIcon.appiconset/100.png -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/AppIcon.appiconset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqy2000/EMURoutingTracker/0a53f38f8a72c08ab9be93715e66be1d45148a1a/EMURoutingTracker/Assets.xcassets/AppIcon.appiconset/1024.png -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/AppIcon.appiconset/114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqy2000/EMURoutingTracker/0a53f38f8a72c08ab9be93715e66be1d45148a1a/EMURoutingTracker/Assets.xcassets/AppIcon.appiconset/114.png -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/AppIcon.appiconset/120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqy2000/EMURoutingTracker/0a53f38f8a72c08ab9be93715e66be1d45148a1a/EMURoutingTracker/Assets.xcassets/AppIcon.appiconset/120.png -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/AppIcon.appiconset/144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqy2000/EMURoutingTracker/0a53f38f8a72c08ab9be93715e66be1d45148a1a/EMURoutingTracker/Assets.xcassets/AppIcon.appiconset/144.png -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/AppIcon.appiconset/152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqy2000/EMURoutingTracker/0a53f38f8a72c08ab9be93715e66be1d45148a1a/EMURoutingTracker/Assets.xcassets/AppIcon.appiconset/152.png -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/AppIcon.appiconset/167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqy2000/EMURoutingTracker/0a53f38f8a72c08ab9be93715e66be1d45148a1a/EMURoutingTracker/Assets.xcassets/AppIcon.appiconset/167.png -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/AppIcon.appiconset/172.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqy2000/EMURoutingTracker/0a53f38f8a72c08ab9be93715e66be1d45148a1a/EMURoutingTracker/Assets.xcassets/AppIcon.appiconset/172.png -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/AppIcon.appiconset/180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqy2000/EMURoutingTracker/0a53f38f8a72c08ab9be93715e66be1d45148a1a/EMURoutingTracker/Assets.xcassets/AppIcon.appiconset/180.png -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/AppIcon.appiconset/196.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqy2000/EMURoutingTracker/0a53f38f8a72c08ab9be93715e66be1d45148a1a/EMURoutingTracker/Assets.xcassets/AppIcon.appiconset/196.png -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/AppIcon.appiconset/20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqy2000/EMURoutingTracker/0a53f38f8a72c08ab9be93715e66be1d45148a1a/EMURoutingTracker/Assets.xcassets/AppIcon.appiconset/20.png -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/AppIcon.appiconset/216.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqy2000/EMURoutingTracker/0a53f38f8a72c08ab9be93715e66be1d45148a1a/EMURoutingTracker/Assets.xcassets/AppIcon.appiconset/216.png -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/AppIcon.appiconset/29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqy2000/EMURoutingTracker/0a53f38f8a72c08ab9be93715e66be1d45148a1a/EMURoutingTracker/Assets.xcassets/AppIcon.appiconset/29.png -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/AppIcon.appiconset/40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqy2000/EMURoutingTracker/0a53f38f8a72c08ab9be93715e66be1d45148a1a/EMURoutingTracker/Assets.xcassets/AppIcon.appiconset/40.png -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/AppIcon.appiconset/48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqy2000/EMURoutingTracker/0a53f38f8a72c08ab9be93715e66be1d45148a1a/EMURoutingTracker/Assets.xcassets/AppIcon.appiconset/48.png -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/AppIcon.appiconset/50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqy2000/EMURoutingTracker/0a53f38f8a72c08ab9be93715e66be1d45148a1a/EMURoutingTracker/Assets.xcassets/AppIcon.appiconset/50.png -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/AppIcon.appiconset/55.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqy2000/EMURoutingTracker/0a53f38f8a72c08ab9be93715e66be1d45148a1a/EMURoutingTracker/Assets.xcassets/AppIcon.appiconset/55.png -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/AppIcon.appiconset/57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqy2000/EMURoutingTracker/0a53f38f8a72c08ab9be93715e66be1d45148a1a/EMURoutingTracker/Assets.xcassets/AppIcon.appiconset/57.png -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/AppIcon.appiconset/58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqy2000/EMURoutingTracker/0a53f38f8a72c08ab9be93715e66be1d45148a1a/EMURoutingTracker/Assets.xcassets/AppIcon.appiconset/58.png -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/AppIcon.appiconset/60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqy2000/EMURoutingTracker/0a53f38f8a72c08ab9be93715e66be1d45148a1a/EMURoutingTracker/Assets.xcassets/AppIcon.appiconset/60.png -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/AppIcon.appiconset/72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqy2000/EMURoutingTracker/0a53f38f8a72c08ab9be93715e66be1d45148a1a/EMURoutingTracker/Assets.xcassets/AppIcon.appiconset/72.png -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/AppIcon.appiconset/76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqy2000/EMURoutingTracker/0a53f38f8a72c08ab9be93715e66be1d45148a1a/EMURoutingTracker/Assets.xcassets/AppIcon.appiconset/76.png -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/AppIcon.appiconset/80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqy2000/EMURoutingTracker/0a53f38f8a72c08ab9be93715e66be1d45148a1a/EMURoutingTracker/Assets.xcassets/AppIcon.appiconset/80.png -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/AppIcon.appiconset/87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqy2000/EMURoutingTracker/0a53f38f8a72c08ab9be93715e66be1d45148a1a/EMURoutingTracker/Assets.xcassets/AppIcon.appiconset/87.png -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/AppIcon.appiconset/88.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqy2000/EMURoutingTracker/0a53f38f8a72c08ab9be93715e66be1d45148a1a/EMURoutingTracker/Assets.xcassets/AppIcon.appiconset/88.png -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "40.png", 5 | "idiom" : "iphone", 6 | "scale" : "2x", 7 | "size" : "20x20" 8 | }, 9 | { 10 | "filename" : "60.png", 11 | "idiom" : "iphone", 12 | "scale" : "3x", 13 | "size" : "20x20" 14 | }, 15 | { 16 | "filename" : "29.png", 17 | "idiom" : "iphone", 18 | "scale" : "1x", 19 | "size" : "29x29" 20 | }, 21 | { 22 | "filename" : "58.png", 23 | "idiom" : "iphone", 24 | "scale" : "2x", 25 | "size" : "29x29" 26 | }, 27 | { 28 | "filename" : "87.png", 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "29x29" 32 | }, 33 | { 34 | "filename" : "80.png", 35 | "idiom" : "iphone", 36 | "scale" : "2x", 37 | "size" : "40x40" 38 | }, 39 | { 40 | "filename" : "120.png", 41 | "idiom" : "iphone", 42 | "scale" : "3x", 43 | "size" : "40x40" 44 | }, 45 | { 46 | "filename" : "57.png", 47 | "idiom" : "iphone", 48 | "scale" : "1x", 49 | "size" : "57x57" 50 | }, 51 | { 52 | "filename" : "114.png", 53 | "idiom" : "iphone", 54 | "scale" : "2x", 55 | "size" : "57x57" 56 | }, 57 | { 58 | "filename" : "120.png", 59 | "idiom" : "iphone", 60 | "scale" : "2x", 61 | "size" : "60x60" 62 | }, 63 | { 64 | "filename" : "180.png", 65 | "idiom" : "iphone", 66 | "scale" : "3x", 67 | "size" : "60x60" 68 | }, 69 | { 70 | "filename" : "20.png", 71 | "idiom" : "ipad", 72 | "scale" : "1x", 73 | "size" : "20x20" 74 | }, 75 | { 76 | "filename" : "40.png", 77 | "idiom" : "ipad", 78 | "scale" : "2x", 79 | "size" : "20x20" 80 | }, 81 | { 82 | "filename" : "29.png", 83 | "idiom" : "ipad", 84 | "scale" : "1x", 85 | "size" : "29x29" 86 | }, 87 | { 88 | "filename" : "58.png", 89 | "idiom" : "ipad", 90 | "scale" : "2x", 91 | "size" : "29x29" 92 | }, 93 | { 94 | "filename" : "40.png", 95 | "idiom" : "ipad", 96 | "scale" : "1x", 97 | "size" : "40x40" 98 | }, 99 | { 100 | "filename" : "80.png", 101 | "idiom" : "ipad", 102 | "scale" : "2x", 103 | "size" : "40x40" 104 | }, 105 | { 106 | "filename" : "50.png", 107 | "idiom" : "ipad", 108 | "scale" : "1x", 109 | "size" : "50x50" 110 | }, 111 | { 112 | "filename" : "100.png", 113 | "idiom" : "ipad", 114 | "scale" : "2x", 115 | "size" : "50x50" 116 | }, 117 | { 118 | "filename" : "72.png", 119 | "idiom" : "ipad", 120 | "scale" : "1x", 121 | "size" : "72x72" 122 | }, 123 | { 124 | "filename" : "144.png", 125 | "idiom" : "ipad", 126 | "scale" : "2x", 127 | "size" : "72x72" 128 | }, 129 | { 130 | "filename" : "76.png", 131 | "idiom" : "ipad", 132 | "scale" : "1x", 133 | "size" : "76x76" 134 | }, 135 | { 136 | "filename" : "152.png", 137 | "idiom" : "ipad", 138 | "scale" : "2x", 139 | "size" : "76x76" 140 | }, 141 | { 142 | "filename" : "167.png", 143 | "idiom" : "ipad", 144 | "scale" : "2x", 145 | "size" : "83.5x83.5" 146 | }, 147 | { 148 | "filename" : "1024.png", 149 | "idiom" : "ios-marketing", 150 | "scale" : "1x", 151 | "size" : "1024x1024" 152 | }, 153 | { 154 | "filename" : "Untitled-2128@0.25x-2.png", 155 | "idiom" : "mac", 156 | "scale" : "1x", 157 | "size" : "16x16" 158 | }, 159 | { 160 | "filename" : "Untitled-2128@0.25x.png", 161 | "idiom" : "mac", 162 | "scale" : "2x", 163 | "size" : "16x16" 164 | }, 165 | { 166 | "filename" : "Untitled-2128@0.25x-1.png", 167 | "idiom" : "mac", 168 | "scale" : "1x", 169 | "size" : "32x32" 170 | }, 171 | { 172 | "filename" : "Untitled-2128@0.5x.png", 173 | "idiom" : "mac", 174 | "scale" : "2x", 175 | "size" : "32x32" 176 | }, 177 | { 178 | "filename" : "Untitled-2128.png", 179 | "idiom" : "mac", 180 | "scale" : "1x", 181 | "size" : "128x128" 182 | }, 183 | { 184 | "filename" : "Untitled-2@0.25x-1.png", 185 | "idiom" : "mac", 186 | "scale" : "2x", 187 | "size" : "128x128" 188 | }, 189 | { 190 | "filename" : "Untitled-2@0.25x.png", 191 | "idiom" : "mac", 192 | "scale" : "1x", 193 | "size" : "256x256" 194 | }, 195 | { 196 | "filename" : "Untitled-2@0.5x-1.png", 197 | "idiom" : "mac", 198 | "scale" : "2x", 199 | "size" : "256x256" 200 | }, 201 | { 202 | "filename" : "Untitled-2@0.5x.png", 203 | "idiom" : "mac", 204 | "scale" : "1x", 205 | "size" : "512x512" 206 | }, 207 | { 208 | "filename" : "Untitled-2.png", 209 | "idiom" : "mac", 210 | "scale" : "2x", 211 | "size" : "512x512" 212 | }, 213 | { 214 | "filename" : "48.png", 215 | "idiom" : "watch", 216 | "role" : "notificationCenter", 217 | "scale" : "2x", 218 | "size" : "24x24", 219 | "subtype" : "38mm" 220 | }, 221 | { 222 | "filename" : "55.png", 223 | "idiom" : "watch", 224 | "role" : "notificationCenter", 225 | "scale" : "2x", 226 | "size" : "27.5x27.5", 227 | "subtype" : "42mm" 228 | }, 229 | { 230 | "filename" : "58.png", 231 | "idiom" : "watch", 232 | "role" : "companionSettings", 233 | "scale" : "2x", 234 | "size" : "29x29" 235 | }, 236 | { 237 | "filename" : "87.png", 238 | "idiom" : "watch", 239 | "role" : "companionSettings", 240 | "scale" : "3x", 241 | "size" : "29x29" 242 | }, 243 | { 244 | "idiom" : "watch", 245 | "role" : "notificationCenter", 246 | "scale" : "2x", 247 | "size" : "33x33", 248 | "subtype" : "45mm" 249 | }, 250 | { 251 | "filename" : "80.png", 252 | "idiom" : "watch", 253 | "role" : "appLauncher", 254 | "scale" : "2x", 255 | "size" : "40x40", 256 | "subtype" : "38mm" 257 | }, 258 | { 259 | "filename" : "88.png", 260 | "idiom" : "watch", 261 | "role" : "appLauncher", 262 | "scale" : "2x", 263 | "size" : "44x44", 264 | "subtype" : "40mm" 265 | }, 266 | { 267 | "idiom" : "watch", 268 | "role" : "appLauncher", 269 | "scale" : "2x", 270 | "size" : "46x46", 271 | "subtype" : "41mm" 272 | }, 273 | { 274 | "filename" : "100.png", 275 | "idiom" : "watch", 276 | "role" : "appLauncher", 277 | "scale" : "2x", 278 | "size" : "50x50", 279 | "subtype" : "44mm" 280 | }, 281 | { 282 | "idiom" : "watch", 283 | "role" : "appLauncher", 284 | "scale" : "2x", 285 | "size" : "51x51", 286 | "subtype" : "45mm" 287 | }, 288 | { 289 | "idiom" : "watch", 290 | "role" : "appLauncher", 291 | "scale" : "2x", 292 | "size" : "54x54", 293 | "subtype" : "49mm" 294 | }, 295 | { 296 | "filename" : "172.png", 297 | "idiom" : "watch", 298 | "role" : "quickLook", 299 | "scale" : "2x", 300 | "size" : "86x86", 301 | "subtype" : "38mm" 302 | }, 303 | { 304 | "filename" : "196.png", 305 | "idiom" : "watch", 306 | "role" : "quickLook", 307 | "scale" : "2x", 308 | "size" : "98x98", 309 | "subtype" : "42mm" 310 | }, 311 | { 312 | "filename" : "216.png", 313 | "idiom" : "watch", 314 | "role" : "quickLook", 315 | "scale" : "2x", 316 | "size" : "108x108", 317 | "subtype" : "44mm" 318 | }, 319 | { 320 | "idiom" : "watch", 321 | "role" : "quickLook", 322 | "scale" : "2x", 323 | "size" : "117x117", 324 | "subtype" : "45mm" 325 | }, 326 | { 327 | "idiom" : "watch", 328 | "role" : "quickLook", 329 | "scale" : "2x", 330 | "size" : "129x129", 331 | "subtype" : "49mm" 332 | }, 333 | { 334 | "filename" : "1024.png", 335 | "idiom" : "watch-marketing", 336 | "scale" : "1x", 337 | "size" : "1024x1024" 338 | } 339 | ], 340 | "info" : { 341 | "author" : "xcode", 342 | "version" : 1 343 | } 344 | } 345 | -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/AppIcon.appiconset/Untitled-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqy2000/EMURoutingTracker/0a53f38f8a72c08ab9be93715e66be1d45148a1a/EMURoutingTracker/Assets.xcassets/AppIcon.appiconset/Untitled-2.png -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/AppIcon.appiconset/Untitled-2128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqy2000/EMURoutingTracker/0a53f38f8a72c08ab9be93715e66be1d45148a1a/EMURoutingTracker/Assets.xcassets/AppIcon.appiconset/Untitled-2128.png -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/AppIcon.appiconset/Untitled-2128@0.25x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqy2000/EMURoutingTracker/0a53f38f8a72c08ab9be93715e66be1d45148a1a/EMURoutingTracker/Assets.xcassets/AppIcon.appiconset/Untitled-2128@0.25x-1.png -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/AppIcon.appiconset/Untitled-2128@0.25x-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqy2000/EMURoutingTracker/0a53f38f8a72c08ab9be93715e66be1d45148a1a/EMURoutingTracker/Assets.xcassets/AppIcon.appiconset/Untitled-2128@0.25x-2.png -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/AppIcon.appiconset/Untitled-2128@0.25x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqy2000/EMURoutingTracker/0a53f38f8a72c08ab9be93715e66be1d45148a1a/EMURoutingTracker/Assets.xcassets/AppIcon.appiconset/Untitled-2128@0.25x.png -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/AppIcon.appiconset/Untitled-2128@0.5x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqy2000/EMURoutingTracker/0a53f38f8a72c08ab9be93715e66be1d45148a1a/EMURoutingTracker/Assets.xcassets/AppIcon.appiconset/Untitled-2128@0.5x.png -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/AppIcon.appiconset/Untitled-2@0.25x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqy2000/EMURoutingTracker/0a53f38f8a72c08ab9be93715e66be1d45148a1a/EMURoutingTracker/Assets.xcassets/AppIcon.appiconset/Untitled-2@0.25x-1.png -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/AppIcon.appiconset/Untitled-2@0.25x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqy2000/EMURoutingTracker/0a53f38f8a72c08ab9be93715e66be1d45148a1a/EMURoutingTracker/Assets.xcassets/AppIcon.appiconset/Untitled-2@0.25x.png -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/AppIcon.appiconset/Untitled-2@0.5x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqy2000/EMURoutingTracker/0a53f38f8a72c08ab9be93715e66be1d45148a1a/EMURoutingTracker/Assets.xcassets/AppIcon.appiconset/Untitled-2@0.5x-1.png -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/AppIcon.appiconset/Untitled-2@0.5x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqy2000/EMURoutingTracker/0a53f38f8a72c08ab9be93715e66be1d45148a1a/EMURoutingTracker/Assets.xcassets/AppIcon.appiconset/Untitled-2@0.5x.png -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/CR200J.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "cn-cr200j.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/CR200J.imageset/cn-cr200j.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqy2000/EMURoutingTracker/0a53f38f8a72c08ab9be93715e66be1d45148a1a/EMURoutingTracker/Assets.xcassets/CR200J.imageset/cn-cr200j.png -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/CR300A.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "cn-cr300af.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/CR300A.imageset/cn-cr300af.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqy2000/EMURoutingTracker/0a53f38f8a72c08ab9be93715e66be1d45148a1a/EMURoutingTracker/Assets.xcassets/CR300A.imageset/cn-cr300af.png -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/CR300B.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "cn-cr300bf.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/CR300B.imageset/cn-cr300bf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqy2000/EMURoutingTracker/0a53f38f8a72c08ab9be93715e66be1d45148a1a/EMURoutingTracker/Assets.xcassets/CR300B.imageset/cn-cr300bf.png -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/CR400A.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "cn-cr400a.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/CR400A.imageset/cn-cr400a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqy2000/EMURoutingTracker/0a53f38f8a72c08ab9be93715e66be1d45148a1a/EMURoutingTracker/Assets.xcassets/CR400A.imageset/cn-cr400a.png -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/CR400B.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "cn-cr400b.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/CR400B.imageset/cn-cr400b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqy2000/EMURoutingTracker/0a53f38f8a72c08ab9be93715e66be1d45148a1a/EMURoutingTracker/Assets.xcassets/CR400B.imageset/cn-cr400b.png -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/CRH1.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "cn-crh1.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/CRH1.imageset/cn-crh1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqy2000/EMURoutingTracker/0a53f38f8a72c08ab9be93715e66be1d45148a1a/EMURoutingTracker/Assets.xcassets/CRH1.imageset/cn-crh1.png -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/CRH1E.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "cn-crh1e.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/CRH1E.imageset/cn-crh1e.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqy2000/EMURoutingTracker/0a53f38f8a72c08ab9be93715e66be1d45148a1a/EMURoutingTracker/Assets.xcassets/CRH1E.imageset/cn-crh1e.png -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/CRH2.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "cn-crh2.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/CRH2.imageset/cn-crh2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqy2000/EMURoutingTracker/0a53f38f8a72c08ab9be93715e66be1d45148a1a/EMURoutingTracker/Assets.xcassets/CRH2.imageset/cn-crh2.png -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/CRH2B.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "cn-crh2b.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/CRH2B.imageset/cn-crh2b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqy2000/EMURoutingTracker/0a53f38f8a72c08ab9be93715e66be1d45148a1a/EMURoutingTracker/Assets.xcassets/CRH2B.imageset/cn-crh2b.png -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/CRH2C.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "cn-crh2c.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/CRH2C.imageset/cn-crh2c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqy2000/EMURoutingTracker/0a53f38f8a72c08ab9be93715e66be1d45148a1a/EMURoutingTracker/Assets.xcassets/CRH2C.imageset/cn-crh2c.png -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/CRH2H.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "cn-crh2h.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/CRH2H.imageset/cn-crh2h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqy2000/EMURoutingTracker/0a53f38f8a72c08ab9be93715e66be1d45148a1a/EMURoutingTracker/Assets.xcassets/CRH2H.imageset/cn-crh2h.png -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/CRH3.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "cn-crh3.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/CRH3.imageset/cn-crh3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqy2000/EMURoutingTracker/0a53f38f8a72c08ab9be93715e66be1d45148a1a/EMURoutingTracker/Assets.xcassets/CRH3.imageset/cn-crh3.png -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/CRH380.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "cn-crh380.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/CRH380.imageset/cn-crh380.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqy2000/EMURoutingTracker/0a53f38f8a72c08ab9be93715e66be1d45148a1a/EMURoutingTracker/Assets.xcassets/CRH380.imageset/cn-crh380.png -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/CRH380B.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "cn-crh380b.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/CRH380B.imageset/cn-crh380b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqy2000/EMURoutingTracker/0a53f38f8a72c08ab9be93715e66be1d45148a1a/EMURoutingTracker/Assets.xcassets/CRH380B.imageset/cn-crh380b.png -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/CRH380C.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "cn-crh380c.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/CRH380C.imageset/cn-crh380c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqy2000/EMURoutingTracker/0a53f38f8a72c08ab9be93715e66be1d45148a1a/EMURoutingTracker/Assets.xcassets/CRH380C.imageset/cn-crh380c.png -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/CRH380D.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "cn-crh380d.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/CRH380D.imageset/cn-crh380d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqy2000/EMURoutingTracker/0a53f38f8a72c08ab9be93715e66be1d45148a1a/EMURoutingTracker/Assets.xcassets/CRH380D.imageset/cn-crh380d.png -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/CRH3A.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "cn-crh3a.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/CRH3A.imageset/cn-crh3a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqy2000/EMURoutingTracker/0a53f38f8a72c08ab9be93715e66be1d45148a1a/EMURoutingTracker/Assets.xcassets/CRH3A.imageset/cn-crh3a.png -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/CRH3G.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "cn-crh3g.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/CRH3G.imageset/cn-crh3g.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqy2000/EMURoutingTracker/0a53f38f8a72c08ab9be93715e66be1d45148a1a/EMURoutingTracker/Assets.xcassets/CRH3G.imageset/cn-crh3g.png -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/CRH5.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "cn-crh5.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/CRH5.imageset/cn-crh5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqy2000/EMURoutingTracker/0a53f38f8a72c08ab9be93715e66be1d45148a1a/EMURoutingTracker/Assets.xcassets/CRH5.imageset/cn-crh5.png -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/CRH6.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "cn-crh6.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/CRH6.imageset/cn-crh6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqy2000/EMURoutingTracker/0a53f38f8a72c08ab9be93715e66be1d45148a1a/EMURoutingTracker/Assets.xcassets/CRH6.imageset/cn-crh6.png -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/CRH6F.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "cn-crh6f.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/CRH6F.imageset/cn-crh6f.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqy2000/EMURoutingTracker/0a53f38f8a72c08ab9be93715e66be1d45148a1a/EMURoutingTracker/Assets.xcassets/CRH6F.imageset/cn-crh6f.png -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/MTR.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "hk-mtr380a.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /EMURoutingTracker/Assets.xcassets/MTR.imageset/hk-mtr380a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqy2000/EMURoutingTracker/0a53f38f8a72c08ab9be93715e66be1d45148a1a/EMURoutingTracker/Assets.xcassets/MTR.imageset/hk-mtr380a.png -------------------------------------------------------------------------------- /EMURoutingTracker/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // EMURoutingTracker 4 | // 5 | // Created by Qingyang Hu on 10/2/20. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ContentView: View { 11 | 12 | var body: some View { 13 | TabView { 14 | QueryView() 15 | .tabItem { 16 | VStack { 17 | Image(systemName: "magnifyingglass") 18 | Text("查询") 19 | } 20 | } 21 | FavoritesView() 22 | .tabItem { 23 | Image(systemName: "bookmark") 24 | Text("收藏") 25 | } 26 | AboutView() 27 | .tabItem { 28 | Image(systemName: "info") 29 | Text("更多") 30 | } 31 | 32 | } 33 | } 34 | } 35 | 36 | struct ContentView_Previews: PreviewProvider { 37 | static var previews: some View { 38 | ContentView() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /EMURoutingTracker/EMURoutingTracker.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.developer.associated-domains 6 | 7 | com.apple.security.app-sandbox 8 | 9 | com.apple.security.application-groups 10 | 11 | group.me.njliner.chinaemu 12 | 13 | com.apple.security.device.camera 14 | 15 | com.apple.security.network.client 16 | 17 | com.apple.security.personal-information.photos-library 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /EMURoutingTracker/EMURoutingTracker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EMURoutingTracker.swift 3 | // EMURoutingTracker 4 | // 5 | // Created by Qingyang Hu on 10/2/20. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct EMURoutingTracker: App { 12 | @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate 13 | 14 | var body: some Scene { 15 | WindowGroup { 16 | ContentView() 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /EMURoutingTracker/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BGTaskSchedulerPermittedIdentifiers 6 | 7 | CFBundleDevelopmentRegion 8 | $(DEVELOPMENT_LANGUAGE) 9 | CFBundleDisplayName 10 | 交路查询 11 | CFBundleExecutable 12 | $(EXECUTABLE_NAME) 13 | CFBundleIdentifier 14 | $(PRODUCT_BUNDLE_IDENTIFIER) 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleName 18 | $(PRODUCT_NAME) 19 | CFBundlePackageType 20 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 21 | CFBundleShortVersionString 22 | $(MARKETING_VERSION) 23 | CFBundleVersion 24 | $(CURRENT_PROJECT_VERSION) 25 | ITSAppUsesNonExemptEncryption 26 | 27 | LSApplicationCategoryType 28 | public.app-category.utilities 29 | LSRequiresIPhoneOS 30 | 31 | NSCameraUsageDescription 32 | 扫描二维码需要使用您的相机。 33 | UIApplicationSceneManifest 34 | 35 | UIApplicationSupportsMultipleScenes 36 | 37 | 38 | UIApplicationSupportsIndirectInputEvents 39 | 40 | UILaunchScreen 41 | 42 | UIRequiredDeviceCapabilities 43 | 44 | armv7 45 | 46 | UISupportedInterfaceOrientations 47 | 48 | UIInterfaceOrientationPortrait 49 | UIInterfaceOrientationLandscapeLeft 50 | UIInterfaceOrientationLandscapeRight 51 | UIInterfaceOrientationPortraitUpsideDown 52 | 53 | UISupportedInterfaceOrientations~ipad 54 | 55 | UIInterfaceOrientationPortrait 56 | UIInterfaceOrientationPortraitUpsideDown 57 | UIInterfaceOrientationLandscapeLeft 58 | UIInterfaceOrientationLandscapeRight 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /EMURoutingTracker/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /EMURoutingTracker/Util/Query.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Query.swift 3 | // EMURoutingTracker 4 | // 5 | // Created by Qingyang Hu on 12/14/24. 6 | // 7 | 8 | import Foundation 9 | 10 | enum Query { 11 | case remainingTickets(depature: Station, arrival: Station, date: Date) 12 | case trainOrEmu(trainOrEmu: String) 13 | } 14 | 15 | extension Query: Hashable { 16 | func hash(into hasher: inout Hasher) { 17 | switch self { 18 | case .remainingTickets(let departure, let arrival, let date): 19 | hasher.combine(departure) 20 | hasher.combine(arrival) 21 | hasher.combine(date) 22 | case .trainOrEmu(let trainOrEmu): 23 | hasher.combine(trainOrEmu) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /EMURoutingTracker/View/Button/FavoriteButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FavoriteStarView.swift 3 | // EMURoutingTracker 4 | // 5 | // Created by Qingyang Hu on 12/14/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct FavoriteButton: View { 11 | @State var selected: Bool 12 | let trainOrEMU: String 13 | let provider: FavoritesProvider 14 | 15 | init(trainOrEMU: String, provider: FavoritesProvider) { 16 | self.trainOrEMU = trainOrEMU 17 | self.provider = provider 18 | _selected = State(initialValue: provider.contains(trainOrEMU)) 19 | } 20 | 21 | var body: some View { 22 | Button(action: { 23 | if !provider.contains(trainOrEMU) { 24 | provider.add(trainOrEMU) 25 | selected = true 26 | } else { 27 | provider.delete(trainOrEMU) 28 | selected = false 29 | } 30 | }, label: { 31 | if selected { 32 | Image(systemName: "star.fill") 33 | } else { 34 | Image(systemName: "star") 35 | } 36 | }) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /EMURoutingTracker/View/Button/ScanQRCodeButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QRView.swift 3 | // EMURoutingTracker 4 | // 5 | // Created by Qingyang Hu on 1/9/22. 6 | // 7 | 8 | import SwiftUI 9 | import SFSafeSymbols 10 | import AVFoundation 11 | import CodeScanner 12 | 13 | struct ScanQRCodeButton: View { 14 | @State var showSheet = false 15 | @EnvironmentObject var vm: EMUTrainViewModel 16 | 17 | var body: some View { 18 | Button(action: { 19 | showSheet = true 20 | }, label: { 21 | Image(systemName: "qrcode.viewfinder") 22 | }).scanQrCodeActionSheet(isPresented: $showSheet) { message in 23 | vm.postTrackingURL(url: message) 24 | } 25 | } 26 | } 27 | 28 | struct QRView_Previews: PreviewProvider { 29 | static var previews: some View { 30 | ScanQRCodeButton() 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /EMURoutingTracker/View/List/DepartureArrivalList.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DepartureArrivalList.swift 3 | // EMURoutingTracker 4 | // 5 | // Created by Qingyang Hu on 11/20/20. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct DepartureArrivalList: View { 11 | @ObservedObject var vm = DepartureArrivalViewModel() 12 | 13 | let departure: String 14 | let arrival: String 15 | let date: Date 16 | 17 | @Binding var path: NavigationPath 18 | 19 | var body: some View { 20 | List { 21 | ForEach(vm.departureArrivals, id: \.id) { (departureArrival) in 22 | DepartureArrivalRow(path: $path, departureArrival: departureArrival.v1, emu: vm.emuTrainAssocs.first(where: {$0.train == departureArrival.v1.trainNo})) 23 | } 24 | }.onAppear(perform: { 25 | if vm.departureArrivals.isEmpty { 26 | vm.getLeftTickets(from: departure, to: arrival, date: date) 27 | } 28 | }) 29 | .overlay(content: { 30 | if vm.departureArrivals.isEmpty && vm.isLoading { 31 | ProgressView() 32 | } 33 | }).navigationTitle("发着查询") 34 | } 35 | } 36 | 37 | struct LeftTicketsView_Previews: PreviewProvider { 38 | static var previews: some View { 39 | DepartureArrivalList(departure: "BJP", arrival: "SHH", date: Date(), path: Binding.constant(NavigationPath())) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /EMURoutingTracker/View/List/MultipleEMUList.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MultipleEMUsView.swift 3 | // EMURoutingTracker 4 | // 5 | // Created by Qingyang Hu on 11/15/20. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct MultipleEMUList: View { 11 | @EnvironmentObject var vm: EMUTrainViewModel 12 | @Binding var path: NavigationPath 13 | 14 | var body: some View { 15 | List { 16 | ForEach(vm.groupedByDay.keys.sorted().reversed(), id: \.self) { key in 17 | Section(header: Text(key)) { 18 | ForEach(vm.groupedByDay[key] ?? [], id: \.id) { emu in 19 | EMUAndTrainRow(emuTrainAssoc: emu, path: $path, layoutStyle: .emuFirst) 20 | } 21 | } 22 | } 23 | } 24 | .listStyle(InsetGroupedListStyle()) 25 | .toolbar { 26 | ToolbarItem(placement: .principal) { 27 | HStack { 28 | Text(vm.query).font(.headline) 29 | } 30 | } 31 | } 32 | } 33 | } 34 | 35 | struct MultipleEMUsView_Previews: PreviewProvider { 36 | static var previews: some View { 37 | MultipleEMUList(path: Binding.constant(NavigationPath())) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /EMURoutingTracker/View/List/SingleEMUList.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SingleEMUView.swift 3 | // EMURoutingTracker 4 | // 5 | // Created by Qingyang Hu on 11/15/20. 6 | // 7 | 8 | import SwiftUI 9 | import UIKit 10 | 11 | struct SingleEMUList: View { 12 | @EnvironmentObject var vm: EMUTrainViewModel 13 | @Binding var path: NavigationPath 14 | 15 | var body: some View { 16 | List { 17 | ForEach(vm.groupedByDay.keys.sorted().reversed(), id: \.self) { key in 18 | Section(header: Text(key)) { 19 | ForEach(vm.groupedByDay[key] ?? [], id: \.id) { emu in 20 | EMURow(emu: emu, path: $path) 21 | } 22 | } 23 | } 24 | } 25 | .listStyle(InsetGroupedListStyle()) 26 | .toolbar { 27 | ToolbarItem(placement: .principal) { 28 | HStack { 29 | Image(vm.emuTrainAssocList.first?.image ?? "").resizable().scaledToFit().frame(height: 28) 30 | Text(vm.query).font(.headline) 31 | } 32 | } 33 | } 34 | .navigationBarItems(trailing: HStack { 35 | ScanQRCodeButton().environmentObject(vm) 36 | if let emu = vm.emuTrainAssocList.first?.emu { 37 | FavoriteButton(trainOrEMU: emu, provider: FavoritesProvider.EMUs) 38 | } 39 | }) 40 | } 41 | } 42 | 43 | struct SingleEMUView_Previews: PreviewProvider { 44 | static var previews: some View { 45 | SingleEMUList(path: Binding.constant(NavigationPath())) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /EMURoutingTracker/View/List/TrainList.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SingleTrainView.swift 3 | // EMURoutingTracker 4 | // 5 | // Created by Qingyang Hu on 11/15/20. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct TrainList: View { 11 | @EnvironmentObject var vm: EMUTrainViewModel 12 | @Binding var path: NavigationPath 13 | 14 | var body: some View { 15 | List { 16 | ForEach(vm.emuTrainAssocList, id: \.id) { emu in 17 | TrainRow(train: emu, path: $path) 18 | } 19 | } 20 | .listStyle(PlainListStyle()) 21 | .toolbar { 22 | ToolbarItem(placement: .principal) { 23 | VStack { 24 | if let trainInfo = vm.emuTrainAssocList.first?.trainInfo { 25 | Text("\(vm.query)").font(.headline) 26 | Text("\(trainInfo.from) ⇀ \(trainInfo.to)").font(.caption2) 27 | } else { 28 | Text(vm.query).font(.headline) 29 | } 30 | } 31 | } 32 | } 33 | .navigationBarItems(trailing: HStack { 34 | ScanQRCodeButton().environmentObject(vm) 35 | if let train = vm.emuTrainAssocList.first?.singleTrain { 36 | FavoriteButton(trainOrEMU: train, provider: FavoritesProvider.trains) 37 | } 38 | }) 39 | } 40 | } 41 | 42 | struct SingleTrainView_Previews: PreviewProvider { 43 | static var previews: some View { 44 | TrainList(path: Binding.constant(NavigationPath())) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /EMURoutingTracker/View/List/TrainOrEMUView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MoerailView.swift 3 | // EMURoutingTracker 4 | // 5 | // Created by Qingyang Hu on 11/8/20. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct TrainOrEMUView: View { 11 | @StateObject var vm = EMUTrainViewModel() 12 | @Environment(\.presentationMode) var presentationMode: Binding 13 | let query: String 14 | @Binding var path: NavigationPath 15 | 16 | var body: some View { 17 | HStack { 18 | switch vm.mode { 19 | case .singleEmu: 20 | SingleEMUList(path: $path).environmentObject(vm) 21 | case .singleTrain: 22 | TrainList(path: $path).environmentObject(vm) 23 | case .multipleEmus: 24 | MultipleEMUList(path: $path).environmentObject(vm) 25 | default: 26 | EmptyRow(path: $path).environmentObject(vm) 27 | } 28 | } 29 | .navigationBarTitleDisplayMode(.inline) 30 | .onAppear(perform: { 31 | if vm.mode == .loading { 32 | vm.getTrackingRecord(keyword: query) 33 | } 34 | }) 35 | .navigationTitle(query) 36 | } 37 | } 38 | 39 | struct MoerailView_Previews: PreviewProvider { 40 | static var previews: some View { 41 | TrainOrEMUView(query: "380", path: Binding.constant(NavigationPath())) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /EMURoutingTracker/View/Picker/StationPicker.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct StationPicker: View { 4 | let stations: [Station] 5 | let completion: (Station) -> Void 6 | @State private var searchText = "" 7 | @Environment(\.presentationMode) var presentationMode: Binding 8 | 9 | init(_ stations: [Station], completion: @escaping (Station) -> Void) { 10 | self.stations = stations 11 | self.completion = completion 12 | } 13 | 14 | var body: some View { 15 | List { 16 | ForEach(stations.filter{ 17 | $0.name.contains(searchText.replacingOccurrences(of: " ", with: "")) || 18 | $0.pinyin.contains(searchText.replacingOccurrences(of: " ", with: "").lowercased()) || 19 | $0.abbreviation.contains(searchText.replacingOccurrences(of: " ", with: "").lowercased()) || 20 | $0.code.contains(searchText.replacingOccurrences(of: " ", with: "").uppercased()) || 21 | searchText == ""}, id: \.code) { station in 22 | Button(action: { 23 | completion(station) 24 | presentationMode.wrappedValue.dismiss() 25 | }) { 26 | HStack { 27 | VStack(alignment: .leading) { 28 | Text(station.name) 29 | Text(station.pinyin).font(.system(.caption2, design: .monospaced)) 30 | } 31 | Spacer() 32 | Text(station.code).font(.system(.body, design: .monospaced)) 33 | } 34 | } 35 | } 36 | } 37 | .navigationTitle("车站选择") 38 | .searchable(text: $searchText, placement: UIDevice.current.userInterfaceIdiom == .phone ? .navigationBarDrawer(displayMode: .always) : .automatic, prompt: "站名/拼音/拼音首字母") 39 | } 40 | } 41 | 42 | 43 | 44 | struct SearchListView_Previews: PreviewProvider { 45 | static var previews: some View { 46 | StationPicker([Station(name: "南京南", code: "NJN", pinyin: "nanjingnan", abbreviation: "NJN")], completion: { station in 47 | 48 | }) 49 | .environment(\.colorScheme, .light) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /EMURoutingTracker/View/Row/DepartureArrivalRow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DepartureArrivalRow.swift 3 | // EMURoutingTracker 4 | // 5 | // Created by Qingyang Hu on 11/20/20. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct DepartureArrivalRow: View { 11 | @Binding var path: NavigationPath 12 | let departureArrival: DepartureArrival 13 | let emu: EMUTrainAssociation? 14 | 15 | var body: some View { 16 | HStack(spacing: 0) { 17 | Image(emu?.image ?? "").resizable().scaledToFit().frame(width: 20, alignment: .leading) 18 | Spacer().frame(minWidth: 3, maxWidth: 20) 19 | VStack(alignment: .leading) { 20 | Button { 21 | path.append(Query.trainOrEmu(trainOrEmu: departureArrival.trainNo)) 22 | } label: { 23 | Text(departureArrival.trainNo) 24 | .font(.system(.title3, design: .monospaced)) 25 | }.buttonStyle(.borderless) 26 | .frame(width: 100, alignment: .leading) 27 | 28 | if !departureArrival.isEMU { 29 | Text("非动车组") 30 | .foregroundColor(.gray) 31 | .font(Font.caption) 32 | } else if let emu = emu { 33 | Button { 34 | path.append(Query.trainOrEmu(trainOrEmu: emu.emu)) 35 | } label: { 36 | Text(emu.emu) 37 | .lineLimit(1) 38 | .foregroundColor(emu.color) 39 | .fixedSize() 40 | .font(.system(.caption, design: .monospaced)) 41 | }.buttonStyle(.borderless) 42 | } else { 43 | Text("未知") 44 | .lineLimit(1) 45 | .foregroundColor(.gray) 46 | .fixedSize() 47 | .font(.system(.caption)) 48 | } 49 | } 50 | 51 | VStack(alignment: .leading) { 52 | Text(departureArrival.departureStation) 53 | .font(.callout) 54 | .frame(maxWidth: .infinity, alignment: .leading) 55 | Text(departureArrival.departureTime) 56 | .font(.system(.callout, design: .monospaced)) 57 | }.frame(width: 90) 58 | 59 | Spacer(minLength: 3) 60 | Image(systemName: "arrow.right") 61 | Spacer(minLength: 3) 62 | VStack(alignment: .trailing) { 63 | Text(departureArrival.arrivalStation) 64 | .font(.callout) 65 | .fixedSize() 66 | .frame(maxWidth: .infinity, alignment: .trailing) 67 | Text(departureArrival.arrivalTime) 68 | .font(.system(.callout, design: .monospaced)) 69 | .fixedSize() 70 | }.frame(width: 90) 71 | } 72 | } 73 | } 74 | 75 | struct DepartureArrivalRow_Previews: PreviewProvider { 76 | static var previews: some View { 77 | List { 78 | DepartureArrivalRow(path: Binding.constant(NavigationPath()), departureArrival: DepartureArrival(departureTime: "12:00", departureStation: "南京南", arrivalTime: "12:59", arrivalStation: "上海虹桥", trainNo: "G1245"), emu: EMUTrainAssociation(emu: "CRH2A2001325", train: "G123", date: "2020-12-21")) 79 | DepartureArrivalRow(path: Binding.constant(NavigationPath()), departureArrival: DepartureArrival(departureTime: "06:00", departureStation: "南京南", arrivalTime: "12:59", arrivalStation: "上海 虹桥", trainNo: "Z1245"), emu: nil) 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /EMURoutingTracker/View/Row/EMUAndTrainRow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GeneralView.swift 3 | // EMURoutingTracker 4 | // 5 | // Created by Qingyang Hu on 11/20/20. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct EMUAndTrainRow: View { 11 | let emuTrainAssoc: EMUTrainAssociation 12 | @Binding var path: NavigationPath 13 | let layoutStyle: LayoutStyle 14 | 15 | enum LayoutStyle { 16 | case emuFirst 17 | case trainFirst 18 | } 19 | 20 | var body: some View { 21 | HStack { 22 | HStack { 23 | Image(emuTrainAssoc.image) 24 | .resizable() 25 | .scaledToFit() 26 | .frame(height: 28) 27 | 28 | Text(emuTrainAssoc.emu) 29 | .foregroundColor(emuTrainAssoc.color) 30 | .font(.system(.body, design: .monospaced)) 31 | .onTapGesture { 32 | path.append(Query.trainOrEmu(trainOrEmu: emuTrainAssoc.emu)) 33 | } 34 | } 35 | Spacer(minLength: 0) 36 | VStack(alignment: .trailing, spacing: 4) { 37 | Text(emuTrainAssoc.train) 38 | .font(.system(.callout, design: .monospaced)) 39 | .foregroundStyle(.blue) 40 | .onTapGesture { 41 | path.append(Query.trainOrEmu(trainOrEmu: emuTrainAssoc.train)) 42 | } 43 | if let trainInfo = emuTrainAssoc.trainInfo { 44 | Text("\(trainInfo.from) ⇀ \(trainInfo.to)") 45 | .font(.system(.caption2, design: .monospaced)) 46 | } else { 47 | ProgressView() 48 | } 49 | } 50 | } 51 | .environment(\.layoutDirection, layoutStyle == .emuFirst ? .leftToRight : .rightToLeft) 52 | } 53 | } 54 | 55 | #Preview { 56 | EMUAndTrainRow( 57 | emuTrainAssoc: EMUTrainAssociation( 58 | emu: "CRH2A2001", 59 | train: "G2", 60 | date: "2020-12-01" 61 | ), 62 | path: Binding.constant(NavigationPath()), 63 | layoutStyle: .emuFirst 64 | ) 65 | 66 | EMUAndTrainRow( 67 | emuTrainAssoc: EMUTrainAssociation( 68 | emu: "CRH2A2001", 69 | train: "G2", 70 | date: "2020-12-01" 71 | ), 72 | path: Binding.constant(NavigationPath()), 73 | layoutStyle: .trainFirst 74 | ) 75 | } 76 | -------------------------------------------------------------------------------- /EMURoutingTracker/View/Row/EMURow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EMUView.swift 3 | // EMURoutingTracker 4 | // 5 | // Created by Qingyang Hu on 11/15/20. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct EMURow: View { 11 | @State var activeLink: Int? = nil 12 | let emu: EMUTrainAssociation 13 | @Binding var path: NavigationPath 14 | 15 | var body: some View { 16 | HStack { 17 | Button { 18 | path.append(Query.trainOrEmu(trainOrEmu: emu.train)) 19 | } label: { 20 | Text(emu.train).font(.system(.body, design: .monospaced)) 21 | } 22 | 23 | Spacer() 24 | 25 | if let trainInfo = emu.trainInfo { 26 | Text("\(trainInfo.from) ⇀ \(trainInfo.to)") 27 | } else { 28 | ProgressView() 29 | } 30 | } 31 | } 32 | } 33 | 34 | struct EMUView_Previews: PreviewProvider { 35 | static var previews: some View { 36 | EMURow(emu: EMUTrainAssociation(emu: "a", train: "a", date: "a"), path: Binding.constant(NavigationPath())) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /EMURoutingTracker/View/Row/EmptyRow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmptyView.swift 3 | // EMURoutingTracker 4 | // 5 | // Created by Qingyang Hu on 11/15/20. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct EmptyRow: View { 11 | @State var query = "" 12 | @State var showActionSheet = false 13 | @Binding var path: NavigationPath 14 | @EnvironmentObject var vm: EMUTrainViewModel 15 | 16 | var body: some View { 17 | VStack(spacing: 10) { 18 | switch vm.mode { 19 | case .emptyEmu: 20 | Image(systemSymbol: .tram).resizable().aspectRatio(contentMode: .fit).frame(width: 30, height: 30, alignment: .center) 21 | Text("暂未收录\"\(vm.query)\"").foregroundColor(.gray) 22 | Button("扫描点餐二维码,上报车辆信息") { 23 | showActionSheet = true 24 | } 25 | .scanQrCodeActionSheet(isPresented: $showActionSheet) { url in 26 | vm.postTrackingURL(url: url) 27 | } 28 | case .emptyTrain: 29 | Image(systemSymbol: .tram).resizable().aspectRatio(contentMode: .fit).frame(width: 30, height: 30, alignment: .center) 30 | Text("暂未收录\"\(vm.query)\"\n可尝试搜索相关车组号").foregroundColor(.gray).multilineTextAlignment(.center) 31 | case .error: 32 | Image(systemSymbol: .multiply).resizable().aspectRatio(contentMode: .fit).frame(width: 30, height: 30, alignment: .center) 33 | Text(vm.errorMessage).foregroundColor(.gray) 34 | default: 35 | ProgressView() 36 | } 37 | } 38 | } 39 | } 40 | 41 | struct EmptyView_Previews: PreviewProvider { 42 | static var previews: some View { 43 | EmptyRow(path: Binding.constant(NavigationPath())) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /EMURoutingTracker/View/Row/TrainRow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TrainView.swift 3 | // EMURoutingTracker 4 | // 5 | // Created by Qingyang Hu on 11/20/20. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct TrainRow: View { 11 | let train: EMUTrainAssociation 12 | @Binding var path: NavigationPath 13 | 14 | var body: some View { 15 | HStack { 16 | Image(train.image).resizable().scaledToFit().frame(height: 28) 17 | Button { 18 | path.append(Query.trainOrEmu(trainOrEmu: train.emu)) 19 | } label: { 20 | Text(train.emu) 21 | .foregroundColor(train.color) 22 | .font(.system(.body, design: .monospaced)) 23 | } 24 | Spacer() 25 | Text(train.date) 26 | .font(Font.caption.monospacedDigit()) 27 | } 28 | } 29 | } 30 | 31 | struct TrainView_Previews: PreviewProvider { 32 | static var previews: some View { 33 | TrainRow(train: EMUTrainAssociation(emu: "CRH2A2001", train: "G2", date: "2020-12-01"), path: Binding.constant(NavigationPath())) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /EMURoutingTracker/View/Tab/AboutView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AboutView.swift 3 | // EMURoutingTracker 4 | // 5 | // Created by Qingyang Hu on 8/8/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct AboutView: View { 11 | @State var showOpenSourceLicense: Bool = false 12 | var body: some View { 13 | List { 14 | Section(header: Text("关于")) { 15 | Button(action: { 16 | guard let url = URL(string: "https://space.bilibili.com/14289681") else { return } 17 | UIApplication.shared.open(url) 18 | }) 19 | { 20 | HStack { 21 | Text("联系作者").font(.footnote) 22 | Spacer() 23 | Text("NJLiner").font(.footnote) 24 | } 25 | } 26 | 27 | Button(action: { 28 | guard let url = URL(string: "https://rail.re/links/#changelog") else { return } 29 | UIApplication.shared.open(url) 30 | }) 31 | { 32 | HStack { 33 | Text("数据来源").font(.footnote) 34 | Spacer() 35 | Text("rail.re").font(.footnote) 36 | } 37 | 38 | } 39 | 40 | Button(action: { 41 | guard let url = URL(string: "https://github.com/hqy2000/EMURoutingTracker") else { return } 42 | UIApplication.shared.open(url) 43 | }) 44 | { 45 | HStack { 46 | VStack(alignment: .leading, spacing: 2) { 47 | Text("源代码").font(.footnote).foregroundColor(.blue) 48 | Text("欢迎在 GitHub 上加星支持 ⭐️").font(.caption2).foregroundColor(.secondary) 49 | } 50 | Spacer() 51 | Text("GitHub").font(.footnote) 52 | } 53 | 54 | } 55 | 56 | Button(action: { 57 | guard let url = URL(string: "https://testflight.apple.com/join/lB9yDHcd") else { return } 58 | UIApplication.shared.open(url) 59 | }) 60 | { 61 | HStack { 62 | Text("TestFlight").font(.footnote) 63 | } 64 | } 65 | 66 | 67 | Button(action: { 68 | showOpenSourceLicense = true 69 | }) 70 | { 71 | HStack { 72 | Text("开源组件许可").font(.footnote) 73 | } 74 | 75 | } 76 | } 77 | }.listStyle(InsetGroupedListStyle()) 78 | .sheet(isPresented: $showOpenSourceLicense, content: { 79 | ScrollView { 80 | Text( 81 | """ 82 | ---------------------------- 83 | https://github.com/hyperoslo/Cache 84 | 85 | Copyright (c) 2015 Hyper Interaktiv AS 86 | 87 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 88 | 89 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 90 | 91 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 92 | 93 | ---------------------------- 94 | https://github.com/sunshinejr/SwiftyUserDefaults 95 | 96 | Copyright (c) 2015-present Radosław Pietruszewski, Łukasz Mróz 97 | 98 | Permission is hereby granted, free of charge, to any person obtaining a copy 99 | of this software and associated documentation files (the "Software"), to deal 100 | in the Software without restriction, including without limitation the rights 101 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 102 | copies of the Software, and to permit persons to whom the Software is 103 | furnished to do so, subject to the following conditions: 104 | 105 | The above copyright notice and this permission notice shall be included in all 106 | copies or substantial portions of the Software. 107 | 108 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 109 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 110 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 111 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 112 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 113 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 114 | SOFTWARE. 115 | 116 | 117 | ---------------------------- 118 | https://github.com/getsentry/sentry-cocoa 119 | 120 | Copyright (c) 2015 Sentry 121 | 122 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 123 | 124 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 125 | 126 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 127 | 128 | ----------------------- 129 | https://github.com/Moya/Moya 130 | 131 | Copyright (c) 2014-present Artsy, Ash Furrow 132 | 133 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 134 | 135 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 136 | 137 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 138 | 139 | ----------------------- 140 | https://github.com/twostraws/CodeScanner 141 | 142 | Copyright (c) 2019 Paul Hudson 143 | 144 | Permission is hereby granted, free of charge, to any person obtaining a copy 145 | of this software and associated documentation files (the "Software"), to deal 146 | in the Software without restriction, including without limitation the rights 147 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 148 | copies of the Software, and to permit persons to whom the Software is 149 | furnished to do so, subject to the following conditions: 150 | 151 | The above copyright notice and this permission notice shall be included in all 152 | copies or substantial portions of the Software. 153 | 154 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 155 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 156 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 157 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 158 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 159 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 160 | SOFTWARE. 161 | """).padding().font(.system(.caption, design: .monospaced)) 162 | } 163 | }) 164 | } 165 | } 166 | 167 | struct AboutView_Previews: PreviewProvider { 168 | static var previews: some View { 169 | AboutView() 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /EMURoutingTracker/View/Tab/FavoritesView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FavoritesView.swift 3 | // EMURoutingTracker 4 | // 5 | // Created by Qingyang Hu on 11/15/20. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct FavoritesView: View { 11 | @ObservedObject var vm = FavoritesViewModel() 12 | @State private var path = NavigationPath() 13 | 14 | var body: some View { 15 | NavigationStack(path: $path) { 16 | List { 17 | Section(header: Text("车次")) { 18 | if vm.favoriteTrains.isEmpty { 19 | Text("您可以在查询时选择收藏某一特定车次(例如G2)。收藏后的车次将在这里显示其最新的运用信息。").font(.caption) 20 | } else { 21 | ForEach(vm.favoriteTrains, id: \.self) { emu in 22 | EMUAndTrainRow(emuTrainAssoc: emu, path: $path, layoutStyle: .trainFirst) 23 | } 24 | } 25 | } 26 | Section(header: Text("动车组"), footer: Text("您也可以在主屏幕上添加“交路查询”小工具,不用打开 App 即可查看收藏列车和动车组的交路信息。")) { 27 | if vm.favoriteEMUs.isEmpty { 28 | Text("您可以在查询时选择收藏某一特定动车组(例如CRH2A2001)。收藏后的动车组将在这里显示其最新的运用信息。").font(.caption) 29 | } else { 30 | ForEach(vm.favoriteEMUs, id: \.self) { emu in 31 | EMUAndTrainRow(emuTrainAssoc: emu, path: $path, layoutStyle: .emuFirst) 32 | } 33 | } 34 | } 35 | } 36 | .listStyle(InsetGroupedListStyle()) 37 | .onAppear(perform: { 38 | vm.refresh() 39 | }) 40 | .queryNavigation(path: $path) 41 | } 42 | } 43 | 44 | } 45 | 46 | struct FavoritesView_Previews: PreviewProvider { 47 | static var previews: some View { 48 | FavoritesView() 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /EMURoutingTracker/View/Tab/QueryView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QueryView.swift 3 | // EMURoutingTracker 4 | // 5 | // Created by Qingyang Hu on 11/20/20. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | import SwiftyUserDefaults 11 | 12 | struct QueryView: View { 13 | @State var query = "" 14 | @State var departure: Station = Defaults[\.lastDeparture] 15 | @State var arrival: Station = Defaults[\.lastArrival] 16 | @State var date = Calendar.current.date(byAdding: .day, value: 1, to: Date())! 17 | @ObservedObject var provider = StationProvider.shared 18 | @State private var path = NavigationPath() 19 | 20 | var body: some View { 21 | NavigationStack(path: $path) { 22 | List { 23 | Section(header: Text("车组/车次查询")) { 24 | TextField("G2/380/CRH2A2001", text: $query).keyboardType(.asciiCapable).textCase(.uppercase) 25 | Button { 26 | path.append(Query.trainOrEmu(trainOrEmu: query)) 27 | } label: { 28 | Text("查询").frame(maxWidth: .infinity, alignment: .center) 29 | } 30 | } 31 | Section(header: Text("发着查询")) { 32 | NavigationLink( 33 | destination: StationPicker(provider.stations, completion: { station in 34 | Defaults[\.lastDeparture] = station 35 | departure = station 36 | }), 37 | label: { 38 | HStack { 39 | Text("出发地") 40 | Spacer() 41 | Text(departure.name) 42 | } 43 | }) 44 | NavigationLink( 45 | destination: StationPicker(provider.stations, completion: { station in 46 | Defaults[\.lastArrival] = station 47 | arrival = station 48 | }), 49 | label: { 50 | HStack{ 51 | Text("目的地") 52 | Spacer() 53 | Text(arrival.name) 54 | } 55 | }) 56 | DatePicker("出发日期", selection: $date, displayedComponents: .date) 57 | Button { 58 | path.append(Query.remainingTickets(depature: departure, arrival: arrival, date: date)) 59 | } label: { 60 | Text("查询").frame(maxWidth: .infinity, alignment: .center) 61 | } 62 | 63 | } 64 | } 65 | .listStyle(InsetGroupedListStyle()) 66 | .queryNavigation(path: $path) 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /EMURoutingTracker/ViewModifier/QueryNavigationModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NavigationDestination.swift 3 | // EMURoutingTracker 4 | // 5 | // Created by Qingyang Hu on 12/14/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct QueryNavigationModifier: ViewModifier { 11 | @Binding var path: NavigationPath 12 | 13 | func body(content: Content) -> some View { 14 | content 15 | .navigationDestination(for: Query.self) { query in 16 | switch query { 17 | case .remainingTickets(let departure, let arrival, let date): 18 | DepartureArrivalList(departure: departure.code, arrival: arrival.code, date: date, path: $path) 19 | case .trainOrEmu(let trainOrEmu): 20 | TrainOrEMUView(query: trainOrEmu, path: $path) 21 | } 22 | } 23 | } 24 | } 25 | 26 | extension View { 27 | func queryNavigation(path: Binding) -> some View { 28 | modifier(QueryNavigationModifier(path: path)) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /EMURoutingTracker/ViewModifier/ScanQRCodeActionSheetModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReportButton.swift 3 | // EMURoutingTracker 4 | // 5 | // Created by Qingyang Hu on 12/14/24. 6 | // 7 | 8 | import SwiftUI 9 | import PhotosUI 10 | import AVFoundation 11 | import CodeScanner 12 | import Sentry 13 | 14 | struct ScanQRCodeActionSheetModifier: ViewModifier { 15 | @Binding var isPresented: Bool 16 | @State var showAlert: Bool = false 17 | @State var result: String? = nil 18 | var onCompletion: (String) -> Void 19 | 20 | func body(content: Content) -> some View { 21 | content 22 | .sheet(isPresented: $isPresented) { 23 | CodeScannerView(codeTypes: [.qr], manualSelect: true, simulatedData: "") { result in 24 | isPresented = false 25 | switch result { 26 | case .success(let code): 27 | onCompletion(code.string) 28 | self.result = nil 29 | case .failure(let error): 30 | SentrySDK.capture(error: error) 31 | if case .permissionDenied = error { 32 | self.result = "您没有开启相机权限,请至 系统设置 - 隐私 中开启。" 33 | } else { 34 | self.result = error.localizedDescription + "请确认您扫描的二维码为点餐码。" 35 | } 36 | } 37 | 38 | DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { 39 | self.showAlert = true 40 | } 41 | } 42 | } 43 | .alert(isPresented: $showAlert, content: { 44 | Alert(title: Text(result == nil ? "上报成功" : "上报失败"), message: Text(result ?? "感谢您的支持,我们将尽快根据您反馈的信息,更新我们的数据!"), dismissButton: .default(Text("好的"))) 45 | }) 46 | } 47 | } 48 | 49 | extension View { 50 | func scanQrCodeActionSheet( 51 | isPresented: Binding, 52 | onCompletion: @escaping (String) -> Void 53 | ) -> some View { 54 | self.modifier(ScanQRCodeActionSheetModifier( 55 | isPresented: isPresented, 56 | onCompletion: onCompletion 57 | )) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /EMURoutingTrackerFramework/EMURoutingTrackerFramework.h: -------------------------------------------------------------------------------- 1 | // 2 | // EMURoutingTrackerFramework.h 3 | // EMURoutingTrackerFramework 4 | // 5 | // Created by Qingyang Hu on 8/7/21. 6 | // 7 | 8 | #import 9 | 10 | //! Project version number for EMURoutingTrackerFramework. 11 | FOUNDATION_EXPORT double EMURoutingTrackerFrameworkVersionNumber; 12 | 13 | //! Project version string for EMURoutingTrackerFramework. 14 | FOUNDATION_EXPORT const unsigned char EMURoutingTrackerFrameworkVersionString[]; 15 | 16 | // In this header, you should import all the public headers of your framework using statements like #import 17 | 18 | 19 | -------------------------------------------------------------------------------- /EMURoutingTrackerFramework/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /EMURoutingTrackerFramework/Model/CRResponse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CRResponse.swift 3 | // EMURoutingTracker 4 | // 5 | // Created by Qingyang Hu on 11/10/20. 6 | // 7 | 8 | import Foundation 9 | 10 | struct CRResponse: Codable { 11 | let data: T 12 | } 13 | -------------------------------------------------------------------------------- /EMURoutingTrackerFramework/Model/DepartureArrival.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DepartureArrival.swift 3 | // EMURoutingTracker 4 | // 5 | // Created by Qingyang Hu on 11/20/20. 6 | // 7 | 8 | import Foundation 9 | 10 | struct DepartureArrivalV2: Codable, Hashable, Identifiable { 11 | var id: String { 12 | return v1.id 13 | } 14 | 15 | let v1: DepartureArrival 16 | 17 | enum CodingKeys: String, CodingKey { 18 | case v1 = "queryLeftNewDTO" 19 | } 20 | } 21 | 22 | struct DepartureArrival: Codable, Hashable, Identifiable { 23 | var id: String { 24 | return trainNo + departureStation + arrivalStation 25 | } 26 | 27 | let departureTime: String 28 | let departureStation: String 29 | let arrivalTime: String 30 | let arrivalStation: String 31 | let trainNo: String 32 | 33 | // let softSeat: String 34 | // let hardSeat: String 35 | // let softSleeper: String 36 | // let hardSleeper: String 37 | // 38 | // let specialClass: String 39 | // let businessClass: String 40 | // let firstClass: String 41 | // let secondClass: String 42 | // 43 | // let noSeat: String 44 | 45 | var isEMU: Bool { 46 | return trainNo.starts(with: "G") || trainNo.starts(with: "D") || trainNo.starts(with: "C") 47 | } 48 | 49 | enum CodingKeys: String, CodingKey { 50 | case departureTime = "start_time" 51 | case departureStation = "from_station_name" 52 | case arrivalTime = "arrive_time" 53 | case arrivalStation = "to_station_name" 54 | case trainNo = "station_train_code" 55 | 56 | // case softSeat = "rz_num" // 软座 57 | // case hardSeat = "yz_num" // 硬座 58 | // case softSleeper = "rw_num" // 软卧 59 | // case hardSleeper = "yw_num" // 硬卧 60 | // 61 | // case specialClass = "tz_num" 62 | // case businessClass = "swz_num" 63 | // case firstClass = "zy_num" // 一等座 64 | // case secondClass = "ze_num" // 二等座 65 | // 66 | // case noSeat = "wz_num" // 无座 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /EMURoutingTrackerFramework/Model/EMUTrainAssociation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EMU.swift 3 | // EMURoutingTracker 4 | // 5 | // Created by Qingyang Hu on 11/8/20. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | struct EMUTrainAssociation: Codable, Hashable, Identifiable { 12 | var id: Int { 13 | return (emu + train + date + (trainInfo?.from ?? "")).hash 14 | } 15 | 16 | let emu: String 17 | let train: String 18 | var singleTrain: String { 19 | return String(train[..<(train.firstIndex(of: "/") ?? train.endIndex)]) 20 | } 21 | let date: String 22 | var trainInfo: Train? = nil 23 | 24 | var image: String { 25 | var filename = "" 26 | switch emu { 27 | case _ where emu.contains("CR400A"): 28 | return "CR400A" 29 | case _ where emu.contains("CR400B"): 30 | return "CR400B" 31 | case _ where emu.contains("CR300A"): 32 | return "CR300A" 33 | case _ where emu.contains("CR300B"): 34 | return "CR300B" 35 | case _ where emu.contains("CR200J"): 36 | return "CR200J" 37 | case _ where emu.contains("CRH1E"): 38 | return "CRH1E" 39 | case _ where emu.contains("CRH1"): 40 | filename = "CRH1" 41 | case _ where emu.contains("CRH2B"): 42 | filename = "CRH2B" 43 | case _ where emu.contains("CRH2C"): 44 | filename = "CRH2C" 45 | case _ where emu.contains("CRH2G"), 46 | _ where emu.contains("CRH2H"): 47 | filename = "CRH2G" 48 | case _ where emu.contains("CRH2"): 49 | filename = "CRH2" 50 | case _ where emu.contains("CRH380B"): 51 | filename = "CRH380B" 52 | case _ where emu.contains("CRH380C"): 53 | filename = "CRH380C" 54 | case _ where emu.contains("CRH380D"): 55 | filename = "CRH380D" 56 | case _ where emu.contains("CRH380"): 57 | filename = "CRH380" 58 | case _ where emu.contains("CRH3A"): 59 | filename = "CRH3A" 60 | case _ where emu.contains("CRH3"): 61 | filename = "CRH3" 62 | case _ where emu.contains("CRH5"): 63 | filename = "CRH5" 64 | case _ where emu.contains("CRH6F"): 65 | filename = "CRH6F" 66 | case _ where emu.contains("CRH6"): 67 | filename = "CRH6" 68 | case _ where emu.contains("MTR"): 69 | filename = "MTR" 70 | default: 71 | filename = "CRH2" 72 | } 73 | return filename 74 | } 75 | 76 | var color: Color { 77 | switch emu { 78 | case let str where str.contains("CR200"): 79 | return .green 80 | case let str where str.contains("CRH"): 81 | return .blue 82 | case let str where str.contains("CR400B") || str.contains("CR300B"): 83 | return .orange 84 | default: 85 | return .red 86 | } 87 | } 88 | 89 | var shortName: String { 90 | return emu.starts(with: "CRH") ? emu.replacingOccurrences(of: "CRH", with: "") : emu.replacingOccurrences(of: "CR", with: "") 91 | } 92 | 93 | var shortTrain: String { 94 | return train.contains("/") ? String(train[.. Bool { 105 | return lhs.hashValue == rhs.hashValue 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /EMURoutingTrackerFramework/Model/Station.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Station.swift 3 | // EMURoutingTracker 4 | // 5 | // Created by Qingyang Hu on 11/20/20. 6 | // 7 | 8 | import Foundation 9 | import SwiftyUserDefaults 10 | 11 | struct Station: Codable, DefaultsSerializable, Hashable { 12 | let name: String 13 | let code: String 14 | let pinyin: String 15 | let abbreviation: String 16 | 17 | init(name: String, code: String, pinyin: String, abbreviation: String) { 18 | self.name = name 19 | self.code = code 20 | self.pinyin = pinyin 21 | self.abbreviation = abbreviation 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /EMURoutingTrackerFramework/Model/Train.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TrainInfo.swift 3 | // EMURoutingTracker 4 | // 5 | // Created by Qingyang Hu on 11/10/20. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Train: Codable, Hashable { 11 | let from: String 12 | let to: String 13 | let train_no: String 14 | let date: String 15 | let station_train_code: String 16 | 17 | enum CodingKeys: String, CodingKey { 18 | case from = "from_station" 19 | case to = "to_station" 20 | case train_no = "train_no" 21 | case date = "date" 22 | case station_train_code = "station_train_code" 23 | } 24 | } 25 | 26 | -------------------------------------------------------------------------------- /EMURoutingTrackerFramework/Provider/AbstractProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AbstractData.swift 3 | // EMURoutingTracker 4 | // 5 | // Created by Qingyang Hu on 11/8/20. 6 | // 7 | 8 | import Foundation 9 | import Moya 10 | import Sentry 11 | import RxSwift 12 | import RxMoya 13 | 14 | class AbstractProvider { 15 | let provider = MoyaProvider() 16 | 17 | /// Makes a request and returns an observable of the decoded response 18 | func request(target: T, type: R.Type) -> Single { 19 | return provider.rx.request(target) 20 | .flatMap { response -> Single in 21 | guard response.statusCode == 200 else { 22 | let error = NetworkError( 23 | response.statusCode, 24 | response.request?.url?.absoluteString ?? "", 25 | String(data: response.data, encoding: .utf8) ?? "" 26 | ) 27 | SentrySDK.capture(error: error) 28 | return Single.error(error) 29 | } 30 | 31 | do { 32 | let decoder = JSONDecoder() 33 | let result = try decoder.decode(R.self, from: response.data) 34 | return Single.just(result) 35 | } catch { 36 | SentrySDK.capture(error: error) 37 | return Single.error(error) 38 | } 39 | } 40 | } 41 | 42 | /// Makes a request and returns an observable of the raw response string 43 | func requestRaw(target: T) -> Single { 44 | return provider.rx.request(target) 45 | .flatMap { response -> Single in 46 | guard response.statusCode == 200 else { 47 | let error = NetworkError( 48 | response.statusCode, 49 | response.request?.url?.absoluteString ?? "", 50 | String(data: response.data, encoding: .utf8) ?? "" 51 | ) 52 | SentrySDK.capture(error: error) 53 | return Single.error(error) 54 | } 55 | let rawString = String(data: response.data, encoding: .utf8) ?? "" 56 | return Single.just(rawString) 57 | } 58 | } 59 | } 60 | 61 | struct NetworkError: LocalizedError { 62 | let code: Int 63 | let title: String 64 | let content: String 65 | 66 | init(_ code: Int, _ title: String, _ content: String) { 67 | self.code = code 68 | self.title = title 69 | self.content = content 70 | } 71 | 72 | var errorDescription: String? { 73 | return "Error \(code): \(title)\n\(content)" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /EMURoutingTrackerFramework/Provider/FavoritesProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FavoritesProvider.swift 3 | // EMURoutingTracker 4 | // 5 | // Created by Qingyang Hu on 11/20/20. 6 | // 7 | 8 | import Foundation 9 | import SwiftyUserDefaults 10 | import WidgetKit 11 | 12 | internal enum FavoritesProvider { 13 | case trains 14 | case EMUs 15 | 16 | private var defaultsKey: DefaultsKey<[Favorite]> { 17 | switch self { 18 | case .trains: 19 | return DefaultsKeys().favoriteTrains 20 | case .EMUs: 21 | return DefaultsKeys().favoriteEMUs 22 | } 23 | } 24 | 25 | public var favorites: [Favorite] { 26 | return Defaults[key: defaultsKey] 27 | } 28 | 29 | public func contains(_ item: String) -> Bool { 30 | if Defaults[key: defaultsKey].contains(where: {$0.name == item}) { 31 | return true 32 | } else { 33 | return false 34 | } 35 | } 36 | 37 | @discardableResult public func add(_ item: String) -> Bool { 38 | if !contains(item) { 39 | Defaults[key: defaultsKey].append(Favorite(item)) 40 | WidgetCenter.shared.reloadAllTimelines() 41 | return true 42 | } else { 43 | return false 44 | } 45 | } 46 | 47 | @discardableResult public func delete(_ item: String) -> Bool { 48 | if let index = Defaults[key: defaultsKey].firstIndex(where: {$0.name == item}) { 49 | Defaults[key: defaultsKey].remove(at: index) 50 | WidgetCenter.shared.reloadAllTimelines() 51 | return true 52 | } else { 53 | return false 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /EMURoutingTrackerFramework/Provider/StationProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MoerailProvider.swift 3 | // EMURoutingTracker 4 | // 5 | // Created by Qingyang Hu on 11/11/20. 6 | // 7 | 8 | import Foundation 9 | import Cache 10 | import JavaScriptCore 11 | import Sentry 12 | import SwiftUI 13 | import RxSwift 14 | 15 | internal class StationProvider: AbstractProvider, ObservableObject { 16 | public static let shared = StationProvider() 17 | @Published public private(set) var stations: [Station] = [] 18 | private let disposeBag = DisposeBag() 19 | 20 | override private init() { 21 | super.init() 22 | self.get() 23 | } 24 | 25 | private func get() { 26 | self.requestRaw(target: .stations) 27 | .observe(on: MainScheduler.instance) 28 | .subscribe(onSuccess: { [weak self] result in 29 | let context = JSContext() 30 | context?.evaluateScript(result) 31 | if let raw = context?.objectForKeyedSubscript("station_names")?.toString() { 32 | let stations: [Station] = raw.split(separator: "@").map { (raw) in 33 | let info = raw.split(separator: "|") 34 | if info.count < 5 { 35 | return Station(name: "", code: "", pinyin: "", abbreviation: "") 36 | } else { 37 | return Station(name: String(info[1]), code: String(info[2]), pinyin: String(info[3]), abbreviation: String(info[4])) 38 | } 39 | } 40 | self?.stations = stations 41 | } else { 42 | SentrySDK.capture(message: "Error decoding stations: \(result)") 43 | } 44 | }, onFailure: { error in 45 | SentrySDK.capture(message: "Error fetching stations: \(error.localizedDescription)") 46 | 47 | }) 48 | .disposed(by: disposeBag) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /EMURoutingTrackerFramework/Provider/TrainInfoProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MoerailProvider.swift 3 | // EMURoutingTracker 4 | // 5 | // Created by Qingyang Hu on 11/11/20. 6 | // 7 | 8 | import Foundation 9 | import Cache 10 | import Sentry 11 | import RxSwift 12 | 13 | internal class TrainInfoProvider: AbstractProvider { 14 | public static let shared = TrainInfoProvider() 15 | private let storage: Storage 16 | private var queue: [(String, (Train) -> Void)] = [] 17 | private var lock: Bool = false 18 | private let disposeBag = DisposeBag() 19 | 20 | override private init() { 21 | let diskConfig = DiskConfig(name: "TrainInfo") 22 | let memoryConfig = MemoryConfig(expiry: .never, countLimit: 30, totalCostLimit: 50) 23 | 24 | storage = try! Storage( 25 | diskConfig: diskConfig, 26 | memoryConfig: memoryConfig, 27 | fileManager: FileManager.default, 28 | transformer: TransformerFactory.forCodable(ofType: Train.self) 29 | ) 30 | 31 | super.init() 32 | } 33 | 34 | internal func get(forTrain train: String, completion: @escaping (Train) -> Void) { 35 | do { 36 | completion(try storage.object(forKey: train)) 37 | } catch { 38 | SentrySDK.capture(error: error) 39 | queue.append((train, completion)) 40 | run() 41 | } 42 | } 43 | 44 | private func run() { 45 | DispatchQueue.global().sync { 46 | if !lock && queue.count > 0{ 47 | lock = true 48 | let top = queue.removeFirst() 49 | execute(train: top.0, completion: top.1) 50 | } 51 | } 52 | } 53 | 54 | public func cancelAll() { 55 | DispatchQueue.global().sync { 56 | queue = [] 57 | } 58 | } 59 | 60 | private func execute(train: String, completion: @escaping (Train) -> Void) { 61 | if let timetable = try? storage.object(forKey: train) { 62 | completion(timetable) 63 | DispatchQueue.global().sync { 64 | lock = false 65 | run() 66 | } 67 | } else { 68 | let df = DateFormatter() 69 | df.dateFormat = "yyyyMMdd" 70 | 71 | request(target: .train(trainNo: train, date: df.string(from: Date())), type: CRResponse<[Train]>.self) 72 | .observe(on: MainScheduler.instance) 73 | .subscribe(onSuccess: { info in 74 | for entry in info.data { 75 | if entry.station_train_code == train { 76 | try? self.storage.setObject(entry, forKey: train, expiry: .date(Date().addingTimeInterval(60 * 60 * 24 * 2))) 77 | completion(entry) 78 | DispatchQueue.global().sync { 79 | self.lock = false 80 | self.run() 81 | } 82 | 83 | return 84 | } 85 | } 86 | 87 | SentrySDK.capture(message: "Unable to find: \(train).") 88 | let entry = Train(from: "未知", to: "未知", train_no: "", date: "", station_train_code: "") 89 | try? self.storage.setObject(entry, forKey: train, expiry: .date(Date().addingTimeInterval(20))) 90 | completion(entry) 91 | 92 | DispatchQueue.global().sync { 93 | self.lock = false 94 | self.run() 95 | } 96 | 97 | }, onFailure: { error in 98 | DispatchQueue.global().sync { 99 | self.lock = false 100 | self.run() 101 | } 102 | }) 103 | .disposed(by: disposeBag) 104 | } 105 | 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /EMURoutingTrackerFramework/Request/CRRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CRRequest.swift 3 | // EMURoutingTracker 4 | // 5 | // Created by Qingyang Hu on 11/10/20. 6 | // 7 | 8 | import Foundation 9 | import Moya 10 | import Alamofire 11 | 12 | enum CRRequest { 13 | case train(trainNo: String, date: String) 14 | case stations 15 | case leftTicketPrice(from: String, to: String, date: String) 16 | } 17 | 18 | extension CRRequest: TargetType { 19 | var baseURL: URL { 20 | switch self { 21 | case .train(_, _): 22 | return URL(string: "https://search.12306.cn")! 23 | default: 24 | return URL(string: "https://kyfw.12306.cn/")! 25 | } 26 | } 27 | 28 | var path: String { 29 | switch self { 30 | case .train(_,_): 31 | return "search/v1/train/search" 32 | case .stations: 33 | return "otn/resources/js/framework/station_name.js" 34 | case .leftTicketPrice(_, _, _): 35 | return "otn/leftTicketPrice/queryAllPublicPrice" 36 | } 37 | } 38 | 39 | var method: Moya.Method { 40 | return .get 41 | } 42 | 43 | var sampleData: Data { 44 | return Data() 45 | } 46 | 47 | var task: Task { 48 | switch self { 49 | case .train(let trainNo, let date): 50 | return .requestParameters(parameters: [ 51 | "keyword": trainNo, 52 | "date": date 53 | ], encoding: URLEncoding.default) 54 | case .stations: 55 | return .requestPlain 56 | case .leftTicketPrice(let from, let to, let date): 57 | return .requestParameters(parameters: [ 58 | "leftTicketDTO.train_date": date, 59 | "leftTicketDTO.from_station": from, 60 | "leftTicketDTO.to_station": to, 61 | "leftTicketDTO.ticket_type": 1, 62 | "randCode": "" 63 | ], encoding: URLEncoding.default) 64 | } 65 | } 66 | 67 | var headers: [String : String]? { 68 | return ["User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"] 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /EMURoutingTrackerFramework/Request/MoerailRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MoerailRequest.swift 3 | // EMURoutingTracker 4 | // 5 | // Created by Qingyang Hu on 11/8/20. 6 | // 7 | 8 | import Foundation 9 | import Moya 10 | 11 | enum MoerailRequest { 12 | case train(keyword: String) 13 | case trains(keywords: [String]) 14 | case emu(keyword: String) 15 | case emus(keywords: [String]) 16 | case qr(emu: String, url: String) 17 | } 18 | 19 | extension MoerailRequest: TargetType { 20 | var baseURL: URL { 21 | return URL(string: "https://api.rail.re/")! 22 | } 23 | 24 | var path: String { 25 | switch self { 26 | case .train(let keyword): 27 | return "train/" + keyword 28 | case .trains(let keywords): 29 | return "train/" + keywords.reduce("", { prev, current in 30 | return prev + "," + current 31 | }) 32 | case .emu(let keyword): 33 | return "emu/\(keyword)" 34 | case .emus(let keywords): 35 | return "emu/" + keywords.reduce("", { prev, current in 36 | return prev + "," + current 37 | }) 38 | case .qr(let emu, _): 39 | return "emu/\(emu)/qr" 40 | } 41 | 42 | } 43 | 44 | var method: Moya.Method { 45 | switch self { 46 | case .qr(_, _): 47 | return .post 48 | default: 49 | return .get 50 | } 51 | } 52 | 53 | var sampleData: Data { 54 | return Data() 55 | } 56 | 57 | var task: Task { 58 | switch self { 59 | case .qr(_, let url): 60 | return .requestParameters(parameters: [ 61 | "url": url 62 | ], encoding: JSONEncoding.default) 63 | default: 64 | return .requestPlain 65 | } 66 | } 67 | 68 | var headers: [String : String]? { 69 | return nil 70 | } 71 | 72 | 73 | } 74 | -------------------------------------------------------------------------------- /EMURoutingTrackerFramework/Util/DateFormatter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DateFormatter.swift 3 | // EMURoutingTracker 4 | // 5 | // Created by Qingyang Hu on 12/14/24. 6 | // 7 | 8 | import Foundation 9 | 10 | extension DateFormatter { 11 | static var standard: DateFormatter { 12 | let formatter = DateFormatter() 13 | formatter.dateFormat = "yyyy-MM-dd" 14 | return formatter 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /EMURoutingTrackerFramework/Util/UserDefaults.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDefaults.swift 3 | // EMURoutingTracker 4 | // 5 | // Created by Qingyang Hu on 11/15/20. 6 | // 7 | 8 | import Foundation 9 | import SwiftyUserDefaults 10 | 11 | extension DefaultsKeys { 12 | var favoriteTrains: DefaultsKey<[Favorite]> {.init("favoriteTrains", defaultValue: []) } 13 | var favoriteEMUs: DefaultsKey<[Favorite]> {.init("favoriteEMUs", defaultValue: []) } 14 | var lastDeparture: DefaultsKey{.init("lastDeparture", defaultValue: Station(name: "北京", code: "BJP", pinyin: "beijing", abbreviation: "bj")) } 15 | var lastArrival: DefaultsKey{.init("lastArrival", defaultValue: Station(name: "上海", code: "SHH", pinyin: "shanghai", abbreviation: "sh")) } 16 | } 17 | 18 | struct Favorite: Codable, Hashable, DefaultsSerializable { 19 | let name: String 20 | let isPushEnabled: Bool 21 | 22 | init(_ name: String) { 23 | self.name = name 24 | self.isPushEnabled = false 25 | } 26 | } 27 | 28 | var Defaults = DefaultsAdapter(defaults: UserDefaults(suiteName: "group.me.njliner.chinaemu")!, keyStore: .init()) 29 | 30 | class UserDefaultsMigrater { 31 | static public func migrate() { 32 | let oldDefaults = DefaultsAdapter(defaults: UserDefaults.standard, keyStore: .init()) 33 | if !oldDefaults[\.favoriteEMUs].isEmpty { 34 | Defaults[\.favoriteEMUs] = oldDefaults[\.favoriteEMUs] 35 | oldDefaults[\.favoriteEMUs] = [] 36 | } 37 | if !oldDefaults[\.favoriteTrains].isEmpty { 38 | Defaults[\.favoriteTrains] = oldDefaults[\.favoriteTrains] 39 | oldDefaults[\.favoriteTrains] = [] 40 | } 41 | } 42 | } 43 | 44 | -------------------------------------------------------------------------------- /EMURoutingTrackerFramework/ViewModel/DepartureArrivalViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DepartureArrivalViewModel.swift 3 | // EMURoutingTracker 4 | // 5 | // Created by Qingyang Hu on 11/10/20. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | import Cache 11 | import RxSwift 12 | 13 | class DepartureArrivalViewModel: ObservableObject { 14 | let crProvider = AbstractProvider() 15 | let moeRailProvider = AbstractProvider() 16 | private let disposeBag = DisposeBag() 17 | @Published var isLoading = true 18 | @Published var departureArrivals: [DepartureArrivalV2] = [] 19 | @Published var emuTrainAssocs: [EMUTrainAssociation] = [] 20 | 21 | public func getLeftTickets(from: String, to: String, date: Date) { 22 | crProvider.request(target: .leftTicketPrice(from: from, to: to, date: DateFormatter.standard.string(from: date)), type: CRResponse<[DepartureArrivalV2]>.self) 23 | .map { $0.data } 24 | .flatMap { [weak self] departureArrivals -> Single<[EMUTrainAssociation]> in 25 | guard let self else { 26 | return Single.just([]) 27 | } 28 | self.departureArrivals = departureArrivals 29 | let trainNumbers = departureArrivals.map { $0.v1.trainNo } 30 | return self.moeRailProvider.request(target: .trains(keywords: trainNumbers), type: [EMUTrainAssociation].self) 31 | } 32 | .subscribe(onSuccess: { [weak self] emus in 33 | self?.emuTrainAssocs = emus 34 | self?.isLoading = false 35 | }, onFailure: { [weak self] error in 36 | self?.isLoading = false 37 | }) 38 | .disposed(by: disposeBag) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /EMURoutingTrackerFramework/ViewModel/EMUTrainViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MoerailData.swift 3 | // EMURoutingTracker 4 | // 5 | // Created by Qingyang Hu on 11/8/20. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | import BackgroundTasks 11 | import UserNotifications 12 | import RxSwift 13 | 14 | class EMUTrainViewModel: ObservableObject { 15 | let moerailProvider = AbstractProvider() 16 | private let disposeBag = DisposeBag() 17 | 18 | @Published var emuTrainAssocList = [EMUTrainAssociation]() 19 | @Published var mode: Mode = .loading 20 | @Published var query = "" 21 | @Published var errorMessage = "" 22 | 23 | enum Mode { 24 | case loading 25 | case emptyTrain 26 | case emptyEmu 27 | case error 28 | case singleTrain 29 | case singleEmu 30 | case multipleEmus 31 | } 32 | 33 | var groupedByDay: [String: [EMUTrainAssociation]] { 34 | return Dictionary(grouping: emuTrainAssocList, by: { $0.date }) 35 | } 36 | 37 | public func postTrackingURL(url: String, completion: (() -> Void)? = nil) { 38 | moerailProvider.request(target: .qr(emu: query, url: url), type: [EMUTrainAssociation].self) 39 | .observe(on: MainScheduler.instance) 40 | .subscribe({ _ in 41 | debugPrint(url) 42 | }) 43 | .disposed(by: disposeBag) 44 | 45 | } 46 | 47 | public func getTrackingRecord(keyword: String) { 48 | TrainInfoProvider.shared.cancelAll() 49 | query = keyword 50 | mode = .loading 51 | if (keyword.trimmingCharacters(in: .whitespaces).isEmpty) { 52 | emuTrainAssocList = [] 53 | mode = .emptyTrain 54 | } else if (keyword.starts(with: "C") && !keyword.starts(with: "CR")) || keyword.starts(with: "G") || keyword.starts(with: "D") { 55 | moerailProvider.request(target: .train(keyword: keyword), type: [EMUTrainAssociation].self) 56 | .observe(on: MainScheduler.instance) 57 | .subscribe(onSuccess: { [weak self] results in 58 | self?.emuTrainAssocList = results 59 | for (index, emu) in results.enumerated() { 60 | TrainInfoProvider.shared.get(forTrain: emu.singleTrain) { (trainInfo) in 61 | if let emuTrainAssocList = self?.emuTrainAssocList, emuTrainAssocList.count > index { 62 | self?.emuTrainAssocList[index].trainInfo = trainInfo 63 | } 64 | } 65 | } 66 | 67 | if self?.emuTrainAssocList.isEmpty ?? true { 68 | self?.mode = .emptyTrain 69 | } else { 70 | self?.mode = .singleTrain 71 | } 72 | }, onFailure: { [weak self] error in 73 | self?.handleError(error) 74 | }).disposed(by: disposeBag) 75 | 76 | } else { 77 | emuTrainAssocList = [] 78 | moerailProvider.request(target: .emu(keyword: keyword), type: [EMUTrainAssociation].self) 79 | .observe(on: MainScheduler.instance) 80 | .subscribe(onSuccess: { [weak self] results in 81 | self?.emuTrainAssocList = results 82 | 83 | for (index, emu) in results.enumerated() { 84 | if index > 0 && self?.emuTrainAssocList[index].emu != self?.emuTrainAssocList[index - 1].emu { 85 | self?.mode = .multipleEmus 86 | } 87 | TrainInfoProvider.shared.get(forTrain: emu.singleTrain) { (trainInfo) in 88 | if let emuTrainAssocList = self?.emuTrainAssocList, emuTrainAssocList.count > index { 89 | self?.emuTrainAssocList[index].trainInfo = trainInfo 90 | } 91 | } 92 | } 93 | 94 | if self?.emuTrainAssocList.isEmpty ?? true { 95 | self?.mode = .emptyEmu 96 | } else if self?.mode == .loading { 97 | self?.mode = .singleEmu 98 | } 99 | }, onFailure: { [weak self] error in 100 | self?.handleError(error) 101 | }).disposed(by: disposeBag) 102 | } 103 | } 104 | 105 | public func handleError(_ error: Error) { 106 | mode = .error 107 | if let error = error as? NetworkError { 108 | if error.code == 503 { 109 | errorMessage = "服务暂时不可用,请稍后再试" 110 | } else if error.code == 404 { 111 | errorMessage = "\"\(query)\"不是一个正确的车次或车组。" 112 | } else { 113 | errorMessage = error.localizedDescription 114 | } 115 | } else { 116 | errorMessage = error.localizedDescription 117 | } 118 | } 119 | 120 | } 121 | -------------------------------------------------------------------------------- /EMURoutingTrackerFramework/ViewModel/FavoritesViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FavouritesProvider.swift 3 | // EMURoutingTracker 4 | // 5 | // Created by Qingyang Hu on 11/20/20. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | import SwiftyUserDefaults 11 | import RxSwift 12 | 13 | class FavoritesViewModel: ObservableObject { 14 | let moerailProvider = AbstractProvider() 15 | @Published var favoriteEMUs: [EMUTrainAssociation] = [] 16 | @Published var favoriteTrains: [EMUTrainAssociation] = [] 17 | private var lastRefresh: Date? = nil 18 | private let disposeBag = DisposeBag() 19 | private let batchSize = 20 20 | 21 | init() { 22 | refresh() 23 | } 24 | 25 | public func refresh(completion: (() -> Void)? = nil) { 26 | // Avoid 503 issues by skipping frequent requests 27 | guard lastRefresh == nil || Date().timeIntervalSince(lastRefresh!) >= 15.0 else { 28 | print("Too frequent, skip this request.") 29 | completion?() 30 | return 31 | } 32 | lastRefresh = Date() 33 | 34 | if favoriteTrains.isEmpty { 35 | favoriteTrains = FavoritesProvider.trains.favorites.map { favorite in 36 | EMUTrainAssociation(emu: "", train: favorite.name, date: "") 37 | } 38 | } 39 | 40 | if favoriteEMUs.isEmpty { 41 | favoriteEMUs = FavoritesProvider.EMUs.favorites.map { favorite in 42 | EMUTrainAssociation(emu: favorite.name, train: "", date: "") 43 | } 44 | } 45 | 46 | 47 | let trainsSingle = queryInBatches( 48 | items: FavoritesProvider.trains.favorites.map { $0.name }, 49 | associationTypeGenerator: { .trains(keywords: $0) } 50 | ).do(onSuccess: { [weak self] result in 51 | self?.favoriteTrains = result 52 | result.enumerated().forEach { index, emu in 53 | TrainInfoProvider.shared.get(forTrain: emu.singleTrain) { trainInfo in 54 | self?.favoriteTrains[index].trainInfo = trainInfo 55 | } 56 | } 57 | }) 58 | 59 | let emusSingle = queryInBatches( 60 | items: FavoritesProvider.EMUs.favorites.map { $0.name }, 61 | associationTypeGenerator: { .emus(keywords: $0) } 62 | ).do(onSuccess: { [weak self] result in 63 | self?.favoriteEMUs = result 64 | result.enumerated().forEach { index, emu in 65 | TrainInfoProvider.shared.get(forTrain: emu.singleTrain) { trainInfo in 66 | self?.favoriteEMUs[index].trainInfo = trainInfo 67 | } 68 | } 69 | }) 70 | 71 | Single.zip(trainsSingle, emusSingle) 72 | .observe(on: MainScheduler.instance) 73 | .subscribe({ _ in 74 | completion?() 75 | }) 76 | .disposed(by: disposeBag) 77 | } 78 | 79 | private func queryInBatches( 80 | items: [String], 81 | associationTypeGenerator: ([String]) -> MoerailRequest 82 | ) -> Single<[EMUTrainAssociation]> { 83 | guard !items.isEmpty else { 84 | return Single.just([]) // Return an empty observable if the list is empty 85 | } 86 | 87 | let batches = items.chunked(into: batchSize) 88 | let observables = batches.map { [weak self] batch -> Observable<[EMUTrainAssociation]> in 89 | guard let self else { 90 | return Observable.just([]) 91 | } 92 | return self.moerailProvider.request(target: associationTypeGenerator(batch), type: [EMUTrainAssociation].self).asObservable() 93 | } 94 | 95 | return Observable.concat(observables) 96 | .reduce([]) { $0 + $1 } 97 | .asSingle() 98 | } 99 | 100 | } 101 | 102 | private extension Array { 103 | func chunked(into size: Int) -> [[Element]] { 104 | stride(from: 0, to: count, by: size).map { Array(self[$0.. FavoritesEntry { 16 | let entry = FavoritesEntry(date: Date(), favoriteTrains: [EMUTrainAssociation(emu: "CRH2C2001", train: "D0001", date: "20210102")], favoriteEmus: [EMUTrainAssociation(emu: "CR400AF0001", train: "G1", date: "20210102")]) 17 | return entry 18 | } 19 | 20 | func getSnapshot(in context: Context, completion: @escaping (FavoritesEntry) -> ()) { 21 | vm.refresh { 22 | let entry = FavoritesEntry(date: Date(), favoriteTrains: vm.favoriteTrains, favoriteEmus: vm.favoriteEMUs) 23 | completion(entry) 24 | } 25 | } 26 | 27 | func getTimeline( in context: Context, completion: @escaping (Timeline) -> ()) { 28 | vm.refresh { 29 | let entry = FavoritesEntry(date: Date(), favoriteTrains: vm.favoriteTrains, favoriteEmus: vm.favoriteEMUs) 30 | let nextUpdateDate = Calendar.current.date(byAdding: .minute, value: 15, to: Date())! 31 | 32 | let timeline = Timeline( 33 | entries: [entry], 34 | policy: .after(nextUpdateDate) 35 | ) 36 | 37 | completion(timeline) 38 | } 39 | 40 | } 41 | } 42 | 43 | struct FavoritesEntry: TimelineEntry { 44 | var date: Date 45 | var favoriteTrains: [EMUTrainAssociation] 46 | var favoriteEmus: [EMUTrainAssociation] 47 | } 48 | 49 | struct WidgetSingleColumnEntryView : View { 50 | var entry: FavoritesEntry 51 | var body: some View { 52 | HStack(alignment: .center,spacing: 0) { 53 | if entry.favoriteTrains.isEmpty && entry.favoriteEmus.isEmpty { 54 | Text("请先在 App 内添加相关收藏").padding().multilineTextAlignment(.center).foregroundColor(.gray).font(.caption) 55 | } else { 56 | VStack(alignment: .leading, spacing: 3) { 57 | ForEach(entry.favoriteTrains.prefix(4), id: \.self) { emu in 58 | EMUView(emu) 59 | } 60 | Divider() 61 | ForEach(entry.favoriteEmus.prefix(4), id: \.self) { emu in 62 | TrainView(emu) 63 | } 64 | }.padding(8) 65 | } 66 | } 67 | 68 | 69 | } 70 | } 71 | 72 | 73 | struct EMUView: View { 74 | let emu: EMUTrainAssociation 75 | 76 | init(_ emu: EMUTrainAssociation) { 77 | self.emu = emu 78 | } 79 | var body: some View { 80 | HStack { 81 | Text(emu.shortName) 82 | .foregroundColor(emu.color) 83 | .font(.system(.caption2, design: .monospaced)) 84 | Spacer() 85 | Image(emu.image).resizable().scaledToFit().frame(height: 10, alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/).padding(.leading, 1).padding(.trailing, -6) 86 | Text(emu.shortTrain) 87 | .font(.system(.caption2, design: .monospaced)) 88 | } 89 | } 90 | } 91 | 92 | struct TrainView: View { 93 | let train: EMUTrainAssociation 94 | 95 | init(_ train: EMUTrainAssociation) { 96 | self.train = train 97 | } 98 | var body: some View { 99 | HStack { 100 | Image(train.image).resizable().scaledToFit().frame(height: 10, alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/).padding(.leading, 1).padding(.trailing, -6) 101 | Text(train.shortTrain) 102 | .font(.system(.caption2, design: .monospaced)) 103 | Spacer() 104 | Text(train.shortName) 105 | .foregroundColor(train.color) 106 | .font(.system(.caption2, design: .monospaced)) 107 | } 108 | } 109 | } 110 | 111 | 112 | 113 | 114 | @main 115 | struct EMURoutingTrackerWidget: Widget { 116 | let kind: String = "widget" 117 | 118 | var body: some WidgetConfiguration { 119 | StaticConfiguration(kind: kind, provider: Provider()) { entry in 120 | WidgetSingleColumnEntryView(entry: entry) 121 | } 122 | .configurationDisplayName("收藏列车实时交路") 123 | .description("展示收藏的列车或车组的实时交路信息。") 124 | .supportedFamilies([.systemSmall]) 125 | } 126 | } 127 | 128 | struct EMURoutingTrackerWidget_Previews: PreviewProvider { 129 | static var previews: some View { 130 | WidgetSingleColumnEntryView(entry: FavoritesEntry(date: Date(), favoriteTrains: [EMUTrainAssociation(emu: "CRH2A0000", train: "D1323/1231", date: "20210101"), EMUTrainAssociation(emu: "CRH2A0000", train: "D1323/1231", date: "20210101"), EMUTrainAssociation(emu: "CRH2A0000", train: "D1323/1231", date: "20210101"), EMUTrainAssociation(emu: "CRH2A0000", train: "D1323/1231", date: "20210101")], favoriteEmus: [EMUTrainAssociation(emu: "CRH2A0000", train: "D1323/1231", date: "20210101"), EMUTrainAssociation(emu: "CRH2A0000", train: "D1323/1231", date: "20210101"), EMUTrainAssociation(emu: "CRH2A0000", train: "D1323/1231", date: "20210101"), EMUTrainAssociation(emu: "CRH2A0000", train: "D1323/1231", date: "20210101"), EMUTrainAssociation(emu: "CRH2A0000", train: "D1323/1231", date: "20210101")])) 131 | .previewContext(WidgetPreviewContext(family: .systemSmall)) 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /EMURoutingTrackerWidget/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | EMURoutingTrackerWidget 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 19 | CFBundleShortVersionString 20 | $(MARKETING_VERSION) 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSExtension 24 | 25 | NSExtensionPointIdentifier 26 | com.apple.widgetkit-extension 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /EMURoutingTrackerWidgetExtension.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.application-groups 8 | 9 | group.me.njliner.chinaemu 10 | 11 | com.apple.security.network.client 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EMU Routing Tracker 2 | 3 | An app that tracks the routing of Electric Multiple Unit (EMU) trains in China. It allows users to query real-time information about the EMUs, including detailed assignment of fleets (with history up to one month). Built with SwiftUI, the app is now available on the [App Store](https://apps.apple.com/us/app/%E5%8A%A8%E8%BD%A6%E7%BB%84%E4%BA%A4%E8%B7%AF%E6%9F%A5%E8%AF%A2/id1471687297) for iPhone, iPad, and Mac. 4 | 5 | - Search for real-time information on trains by train numbers (e.g., G2) and on EMUs by fleet number (e.g., CRH2A-2001). 6 | - Look up multiple records with fuzzy search (e.g., CRH2A will give you the results for all trains belonging to the CRH2A series). 7 | - Check real-time information along with the timetable by providing both the "from" and "to" stations. 8 | - Favorite a train or EMU, which will then appear in the "Favorites" tab. 9 | - Add a widget to your home screen for quick access to your favorites. 10 | - Report inaccurate information by scanning the QR code inside the train. 11 | 12 | The app relies on the backend from [MoeRail](https://rail.re). You can check the source code of the backend at [Arnie97/emu-log](https://github.com/Arnie97/emu-log). 13 | 14 | # 交路查询 15 | 16 | 一款关于中国铁路动车组列车交路的 app。基于 SwiftUI 开发。现已上架 [App Store](https://apps.apple.com/cn/app/%E5%8A%A8%E8%BD%A6%E7%BB%84%E4%BA%A4%E8%B7%AF%E6%9F%A5%E8%AF%A2/id1471687297),可用于 iPhone、iPad,及 Mac。 17 | 18 | - 支持精确车次(如 G2)或车组号(如 CRH2A-2001)查询。 19 | - 支持模糊查询(如 CRH2A,显示 CRH2A 系列所有车辆的运用信息)。 20 | - 支持时刻表查询(提供到发站)。 21 | - 支持收藏列车或动车组。收藏后的列车及动车组会自动显示在收藏页面上,无需再次手动查询。 22 | - 支持桌面小组件,方便查看收藏列车的相关信息。 23 | - 支持通过扫描点餐二维码自动上报不准确的信息。 24 | 25 | 本 app 数据来自 [MoeRail](https://rail.re)。 其源码开放于 [Arnie97/emu-log](https://github.com/Arnie97/emu-log)。 26 | 27 | ![](https://user-images.githubusercontent.com/12138874/148732779-38ef27ff-f9f1-42bd-a5da-7414bae21530.png) | ![](https://user-images.githubusercontent.com/12138874/148732799-8b738924-53bd-4f37-8c8b-8140288d893d.png) 28 | :-------------------------:|:-------------------------: 29 | ![](https://user-images.githubusercontent.com/12138874/148732811-c392cd59-40af-400b-9b8c-96ae9936969f.png) | ![](https://user-images.githubusercontent.com/12138874/148732818-d017da06-4053-457b-ad4f-f8f21b90d887.png) 30 | -------------------------------------------------------------------------------- /ci_scripts/ci_post_xcodebuild.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Don't forget to run 'chmod +x ci_post_xcodebuild.sh' from the terminal after adding this file to your project 4 | 5 | set -e 6 | if [[ -n $CI_ARCHIVE_PATH ]]; 7 | then 8 | 9 | # Install Sentry CLI into the current directory ($CI_PRIMARY_REPOSITORY_PATH/ci_scripts) 10 | export INSTALL_DIR=$PWD 11 | 12 | if [[ $(command -v sentry-cli) == "" ]]; then 13 | curl -sL https://sentry.io/get-cli/ | bash 14 | fi 15 | 16 | # Upload dSYMs 17 | $CI_PRIMARY_REPOSITORY_PATH/ci_scripts/sentry-cli --auth-token $SENTRY_AUTH_TOKEN upload-dif --org $SENTRY_ORG --project $SENTRY_PROJECT $CI_ARCHIVE_PATH 18 | else 19 | echo "Archive path isn't available. Unable to run dSYMs uploading script." 20 | fi 21 | --------------------------------------------------------------------------------