├── .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 |  | 
28 | :-------------------------:|:-------------------------:
29 |  | 
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 |
--------------------------------------------------------------------------------