├── guide.gif
├── Watch
├── Assets.xcassets
│ ├── Contents.json
│ └── AppIcon.appiconset
│ │ ├── Icon-AppleWatch-1024x1024.png
│ │ ├── Icon-AppleWatch-24x24@2x.png
│ │ ├── Icon-AppleWatch-29x29@2x.png
│ │ ├── Icon-AppleWatch-29x29@3x.png
│ │ ├── Icon-AppleWatch-40x40@2x.png
│ │ ├── Icon-AppleWatch-44x44@2x.png
│ │ ├── Icon-AppleWatch-50x50@2x.png
│ │ ├── Icon-AppleWatch-86x86@2x.png
│ │ ├── Icon-AppleWatch-98x98@2x.png
│ │ ├── Icon-AppleWatch-108x108@2x.png
│ │ ├── Icon-AppleWatch-27.5x27.5@2x.png
│ │ └── Contents.json
├── zh-Hans.lproj
│ └── Interface.strings
├── Info.plist
└── Base.lproj
│ └── Interface.storyboard
├── Argus
├── Resources
│ ├── Assets.xcassets
│ │ ├── Contents.json
│ │ ├── gear.imageset
│ │ │ ├── gear.pdf
│ │ │ └── Contents.json
│ │ ├── photo.imageset
│ │ │ ├── photo.pdf
│ │ │ └── Contents.json
│ │ ├── circle.imageset
│ │ │ ├── circle.pdf
│ │ │ └── Contents.json
│ │ ├── qrcode.imageset
│ │ │ ├── qrcode.pdf
│ │ │ └── Contents.json
│ │ ├── lock.fill.imageset
│ │ │ ├── lock.fill.pdf
│ │ │ └── Contents.json
│ │ ├── trash.fill.imageset
│ │ │ ├── trash.fill.pdf
│ │ │ └── Contents.json
│ │ ├── AppIcon.appiconset
│ │ │ ├── AppIcon-iTunes.png
│ │ │ ├── AppIcon-20x20@1x.png
│ │ │ ├── AppIcon-20x20@2x.png
│ │ │ ├── AppIcon-20x20@3x.png
│ │ │ ├── AppIcon-29x29@1x.png
│ │ │ ├── AppIcon-29x29@2x.png
│ │ │ ├── AppIcon-29x29@3x.png
│ │ │ ├── AppIcon-40x40@1x.png
│ │ │ ├── AppIcon-40x40@2x.png
│ │ │ ├── AppIcon-40x40@3x.png
│ │ │ ├── AppIcon-60x60@2x.png
│ │ │ ├── AppIcon-60x60@3x.png
│ │ │ ├── AppIcon-76x76@1x.png
│ │ │ ├── AppIcon-76x76@2x.png
│ │ │ ├── AppIcon-83.5x83.5@2x.png
│ │ │ └── Contents.json
│ │ ├── arrow.clockwise.imageset
│ │ │ ├── arrow.clockwise.pdf
│ │ │ └── Contents.json
│ │ ├── bolt.slash.fill.imageset
│ │ │ ├── bolt.slash.fill.pdf
│ │ │ └── Contents.json
│ │ ├── chevron.backward.imageset
│ │ │ ├── chevron.backward.pdf
│ │ │ └── Contents.json
│ │ ├── qrcode.viewfinder.imageset
│ │ │ ├── qrcode.viewfinder.pdf
│ │ │ └── Contents.json
│ │ ├── square.and.arrow.up.imageset
│ │ │ ├── square.and.arrow.up.pdf
│ │ │ └── Contents.json
│ │ ├── checkmark.circle.fill.imageset
│ │ │ ├── checkmark.circle.fill.pdf
│ │ │ └── Contents.json
│ │ ├── chevron.left.circle.fill.imageset
│ │ │ ├── chevron.left.circle.fill.pdf
│ │ │ └── Contents.json
│ │ ├── exclamationmark.circle.fill.imageset
│ │ │ ├── exclamationmark.circle.fill.pdf
│ │ │ └── Contents.json
│ │ ├── rectangle.stack.badge.plus.imageset
│ │ │ ├── rectangle.stack.badge.plus.pdf
│ │ │ └── Contents.json
│ │ ├── exclamationmark.triangle.fill.imageset
│ │ │ ├── exclamationmark.triangle.fill.pdf
│ │ │ └── Contents.json
│ │ ├── AccentColor.colorset
│ │ │ └── Contents.json
│ │ └── CellColor.colorset
│ │ │ └── Contents.json
│ ├── Settings.bundle
│ │ ├── en.lproj
│ │ │ └── Root.strings
│ │ ├── zh-Hans.lproj
│ │ │ └── Root.strings
│ │ └── Root.plist
│ ├── zh-Hans.lproj
│ │ ├── InfoPlist.strings
│ │ └── Localizable.strings
│ ├── Base.lproj
│ │ ├── InfoPlist.strings
│ │ ├── LaunchScreen.storyboard
│ │ └── Localizable.strings
│ └── Info.plist
├── Views
│ ├── Utils
│ │ ├── AGViewController.h
│ │ ├── AGBlurViewController.h
│ │ ├── AGViewController.m
│ │ ├── AGBlurViewController.m
│ │ ├── AGTableViewCell.h
│ │ └── AGTableViewCell.m
│ ├── Main
│ │ ├── AGMFATableView.h
│ │ ├── AGMainViewController.h
│ │ ├── AGCreatedLabel.h
│ │ ├── AGMFAEmptyView.h
│ │ ├── AGCountdownView.h
│ │ ├── AGCodeView.h
│ │ ├── AGMFATableViewCell.h
│ │ ├── AGCreatedLabel.m
│ │ ├── AGCountdownView.m
│ │ ├── AGMFAEmptyView.m
│ │ ├── AGCodeView.m
│ │ ├── AGMFATableView.m
│ │ ├── AGMainViewController.m
│ │ └── AGMFATableViewCell.m
│ ├── Scan
│ │ └── AGScanViewController.h
│ ├── Export
│ │ ├── AGExportViewController.h
│ │ ├── AGExportTextViewController.h
│ │ ├── AGExportQrcodeViewController.h
│ │ ├── AGExportTextViewController.m
│ │ ├── AGExportTableViewCell.h
│ │ ├── AGExportTableViewCell.m
│ │ ├── AGExportViewController.m
│ │ └── AGExportQrcodeViewController.m
│ ├── Settings
│ │ ├── AGSettingsViewController.h
│ │ ├── AGAcknowledgementsViewController.h
│ │ └── AGAcknowledgementsViewController.m
│ ├── Editor
│ │ ├── AGQRCodeView.h
│ │ ├── AGEditorViewController.h
│ │ ├── AGQRCodeView.m
│ │ └── AGEditorViewController.m
│ └── Web
│ │ ├── AGWebViewRefresher.h
│ │ ├── AGWebViewController.h
│ │ └── AGWebViewRefresher.m
├── Classes
│ ├── AppDelegate.h
│ ├── main.m
│ ├── AppDelegate.m
│ └── Argus.pch
├── Extensions
│ ├── NSURL+AGExt.h
│ ├── UIColor+AGExt.h
│ ├── UIViewController+AGExt.h
│ ├── UIView+AGExt.h
│ ├── UIButton+AGExt.h
│ ├── NSString+AGExt.h
│ ├── UIColor+AGExt.m
│ ├── UIImage+AGExt.h
│ ├── NSData+AGExt.h
│ ├── UIBarButtonItem+AGExt.h
│ ├── NSURL+AGExt.m
│ ├── UIViewController+AGExt.m
│ ├── UIBarButtonItem+AGExt.m
│ ├── UIView+AGExt.m
│ ├── UIImage+AGExt.m
│ ├── UIButton+AGExt.m
│ ├── NSString+AGExt.m
│ └── NSData+AGExt.m
├── Services
│ ├── AGSecurity.h
│ ├── AGDevice.h
│ ├── AGRouter.h
│ ├── AGDevice.m
│ ├── AGTheme.h
│ ├── AGMFAManager.h
│ ├── AGTheme.m
│ └── AGSecurity.m
├── Models
│ ├── AGMFAModel+GPB.h
│ ├── AGFile.h
│ ├── AGMFAStorage.h
│ ├── AGModel.proto
│ ├── AGMFAModel.h
│ ├── AGFile.m
│ ├── AGMFAStorage.m
│ ├── AGMFAModel+GPB.m
│ └── AGMFAModel.m
└── Argus.entitlements
├── Watch Extension
├── Assets.xcassets
│ ├── Contents.json
│ └── Complication.complicationset
│ │ ├── Modular.imageset
│ │ ├── Modular-38mm@2x.png
│ │ ├── Modular-40mm@2x.png
│ │ ├── Modular-42mm@2x.png
│ │ ├── Modular-44mm@2x.png
│ │ └── Contents.json
│ │ ├── Circular.imageset
│ │ ├── Circular-38mm@2x.png
│ │ ├── Circular-40mm@2x.png
│ │ ├── Circular-42mm@2x.png
│ │ ├── Circular-44mm@2x.png
│ │ └── Contents.json
│ │ ├── Utilitarian.imageset
│ │ ├── Utilitarian-38mm@2x.png
│ │ ├── Utilitarian-40mm@2x.png
│ │ ├── Utilitarian-42mm@2x.png
│ │ ├── Utilitarian-44mm@2x.png
│ │ └── Contents.json
│ │ ├── Graphic Bezel.imageset
│ │ ├── GraphicBezel-40mm@2x.png
│ │ ├── GraphicBezel-44mm@2x.png
│ │ └── Contents.json
│ │ ├── Graphic Corner.imageset
│ │ ├── GraphicCorner-40mm@2x.png
│ │ ├── GraphicCorner-44mm@2x.png
│ │ └── Contents.json
│ │ ├── Graphic Circular.imageset
│ │ ├── GraphicCircular-40mm@2x.png
│ │ ├── GraphicCircular-44mm@2x.png
│ │ └── Contents.json
│ │ ├── Graphic Large Rectangular.imageset
│ │ └── Contents.json
│ │ ├── Extra Large.imageset
│ │ └── Contents.json
│ │ ├── Graphic Extra Large.imageset
│ │ └── Contents.json
│ │ └── Contents.json
├── Classes
│ ├── ExtensionDelegate.h
│ ├── ComplicationController.h
│ ├── InterfaceController.h
│ ├── Theme.h
│ ├── Theme.m
│ ├── CodeRowType.h
│ ├── CodeRowType.m
│ ├── ExtensionDelegate.m
│ ├── InterfaceController.m
│ └── ComplicationController.m
└── Info.plist
├── Argus.xcodeproj
└── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ └── IDEWorkspaceChecks.plist
├── Argus.xcworkspace
├── contents.xcworkspacedata
└── xcshareddata
│ └── IDEWorkspaceChecks.plist
├── Podfile.lock
├── Podfile
├── LICENSE
├── README.md
└── .gitignore
/guide.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wizjin/Argus/HEAD/guide.gif
--------------------------------------------------------------------------------
/Watch/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Argus/Resources/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Watch Extension/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Argus/Resources/Assets.xcassets/gear.imageset/gear.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wizjin/Argus/HEAD/Argus/Resources/Assets.xcassets/gear.imageset/gear.pdf
--------------------------------------------------------------------------------
/Argus/Resources/Settings.bundle/en.lproj/Root.strings:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wizjin/Argus/HEAD/Argus/Resources/Settings.bundle/en.lproj/Root.strings
--------------------------------------------------------------------------------
/Argus/Resources/Assets.xcassets/photo.imageset/photo.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wizjin/Argus/HEAD/Argus/Resources/Assets.xcassets/photo.imageset/photo.pdf
--------------------------------------------------------------------------------
/Argus/Resources/Assets.xcassets/circle.imageset/circle.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wizjin/Argus/HEAD/Argus/Resources/Assets.xcassets/circle.imageset/circle.pdf
--------------------------------------------------------------------------------
/Argus/Resources/Assets.xcassets/qrcode.imageset/qrcode.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wizjin/Argus/HEAD/Argus/Resources/Assets.xcassets/qrcode.imageset/qrcode.pdf
--------------------------------------------------------------------------------
/Argus/Resources/Assets.xcassets/lock.fill.imageset/lock.fill.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wizjin/Argus/HEAD/Argus/Resources/Assets.xcassets/lock.fill.imageset/lock.fill.pdf
--------------------------------------------------------------------------------
/Argus/Resources/Assets.xcassets/trash.fill.imageset/trash.fill.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wizjin/Argus/HEAD/Argus/Resources/Assets.xcassets/trash.fill.imageset/trash.fill.pdf
--------------------------------------------------------------------------------
/Argus/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon-iTunes.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wizjin/Argus/HEAD/Argus/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon-iTunes.png
--------------------------------------------------------------------------------
/Watch/Assets.xcassets/AppIcon.appiconset/Icon-AppleWatch-1024x1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wizjin/Argus/HEAD/Watch/Assets.xcassets/AppIcon.appiconset/Icon-AppleWatch-1024x1024.png
--------------------------------------------------------------------------------
/Watch/Assets.xcassets/AppIcon.appiconset/Icon-AppleWatch-24x24@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wizjin/Argus/HEAD/Watch/Assets.xcassets/AppIcon.appiconset/Icon-AppleWatch-24x24@2x.png
--------------------------------------------------------------------------------
/Watch/Assets.xcassets/AppIcon.appiconset/Icon-AppleWatch-29x29@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wizjin/Argus/HEAD/Watch/Assets.xcassets/AppIcon.appiconset/Icon-AppleWatch-29x29@2x.png
--------------------------------------------------------------------------------
/Watch/Assets.xcassets/AppIcon.appiconset/Icon-AppleWatch-29x29@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wizjin/Argus/HEAD/Watch/Assets.xcassets/AppIcon.appiconset/Icon-AppleWatch-29x29@3x.png
--------------------------------------------------------------------------------
/Watch/Assets.xcassets/AppIcon.appiconset/Icon-AppleWatch-40x40@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wizjin/Argus/HEAD/Watch/Assets.xcassets/AppIcon.appiconset/Icon-AppleWatch-40x40@2x.png
--------------------------------------------------------------------------------
/Watch/Assets.xcassets/AppIcon.appiconset/Icon-AppleWatch-44x44@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wizjin/Argus/HEAD/Watch/Assets.xcassets/AppIcon.appiconset/Icon-AppleWatch-44x44@2x.png
--------------------------------------------------------------------------------
/Watch/Assets.xcassets/AppIcon.appiconset/Icon-AppleWatch-50x50@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wizjin/Argus/HEAD/Watch/Assets.xcassets/AppIcon.appiconset/Icon-AppleWatch-50x50@2x.png
--------------------------------------------------------------------------------
/Watch/Assets.xcassets/AppIcon.appiconset/Icon-AppleWatch-86x86@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wizjin/Argus/HEAD/Watch/Assets.xcassets/AppIcon.appiconset/Icon-AppleWatch-86x86@2x.png
--------------------------------------------------------------------------------
/Watch/Assets.xcassets/AppIcon.appiconset/Icon-AppleWatch-98x98@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wizjin/Argus/HEAD/Watch/Assets.xcassets/AppIcon.appiconset/Icon-AppleWatch-98x98@2x.png
--------------------------------------------------------------------------------
/Argus/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wizjin/Argus/HEAD/Argus/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@1x.png
--------------------------------------------------------------------------------
/Argus/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wizjin/Argus/HEAD/Argus/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@2x.png
--------------------------------------------------------------------------------
/Argus/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wizjin/Argus/HEAD/Argus/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@3x.png
--------------------------------------------------------------------------------
/Argus/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wizjin/Argus/HEAD/Argus/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@1x.png
--------------------------------------------------------------------------------
/Argus/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wizjin/Argus/HEAD/Argus/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@2x.png
--------------------------------------------------------------------------------
/Argus/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wizjin/Argus/HEAD/Argus/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@3x.png
--------------------------------------------------------------------------------
/Argus/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wizjin/Argus/HEAD/Argus/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@1x.png
--------------------------------------------------------------------------------
/Argus/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wizjin/Argus/HEAD/Argus/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@2x.png
--------------------------------------------------------------------------------
/Argus/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wizjin/Argus/HEAD/Argus/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@3x.png
--------------------------------------------------------------------------------
/Argus/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon-60x60@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wizjin/Argus/HEAD/Argus/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon-60x60@2x.png
--------------------------------------------------------------------------------
/Argus/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon-60x60@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wizjin/Argus/HEAD/Argus/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon-60x60@3x.png
--------------------------------------------------------------------------------
/Argus/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon-76x76@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wizjin/Argus/HEAD/Argus/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon-76x76@1x.png
--------------------------------------------------------------------------------
/Argus/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon-76x76@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wizjin/Argus/HEAD/Argus/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon-76x76@2x.png
--------------------------------------------------------------------------------
/Watch/Assets.xcassets/AppIcon.appiconset/Icon-AppleWatch-108x108@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wizjin/Argus/HEAD/Watch/Assets.xcassets/AppIcon.appiconset/Icon-AppleWatch-108x108@2x.png
--------------------------------------------------------------------------------
/Argus/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon-83.5x83.5@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wizjin/Argus/HEAD/Argus/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon-83.5x83.5@2x.png
--------------------------------------------------------------------------------
/Watch/Assets.xcassets/AppIcon.appiconset/Icon-AppleWatch-27.5x27.5@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wizjin/Argus/HEAD/Watch/Assets.xcassets/AppIcon.appiconset/Icon-AppleWatch-27.5x27.5@2x.png
--------------------------------------------------------------------------------
/Argus/Resources/Assets.xcassets/arrow.clockwise.imageset/arrow.clockwise.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wizjin/Argus/HEAD/Argus/Resources/Assets.xcassets/arrow.clockwise.imageset/arrow.clockwise.pdf
--------------------------------------------------------------------------------
/Argus/Resources/Assets.xcassets/bolt.slash.fill.imageset/bolt.slash.fill.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wizjin/Argus/HEAD/Argus/Resources/Assets.xcassets/bolt.slash.fill.imageset/bolt.slash.fill.pdf
--------------------------------------------------------------------------------
/Argus/Resources/Assets.xcassets/chevron.backward.imageset/chevron.backward.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wizjin/Argus/HEAD/Argus/Resources/Assets.xcassets/chevron.backward.imageset/chevron.backward.pdf
--------------------------------------------------------------------------------
/Argus/Resources/Assets.xcassets/qrcode.viewfinder.imageset/qrcode.viewfinder.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wizjin/Argus/HEAD/Argus/Resources/Assets.xcassets/qrcode.viewfinder.imageset/qrcode.viewfinder.pdf
--------------------------------------------------------------------------------
/Argus/Resources/Assets.xcassets/square.and.arrow.up.imageset/square.and.arrow.up.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wizjin/Argus/HEAD/Argus/Resources/Assets.xcassets/square.and.arrow.up.imageset/square.and.arrow.up.pdf
--------------------------------------------------------------------------------
/Argus.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Argus/Resources/Assets.xcassets/checkmark.circle.fill.imageset/checkmark.circle.fill.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wizjin/Argus/HEAD/Argus/Resources/Assets.xcassets/checkmark.circle.fill.imageset/checkmark.circle.fill.pdf
--------------------------------------------------------------------------------
/Argus/Resources/Assets.xcassets/chevron.left.circle.fill.imageset/chevron.left.circle.fill.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wizjin/Argus/HEAD/Argus/Resources/Assets.xcassets/chevron.left.circle.fill.imageset/chevron.left.circle.fill.pdf
--------------------------------------------------------------------------------
/Argus/Resources/Settings.bundle/zh-Hans.lproj/Root.strings:
--------------------------------------------------------------------------------
1 | /*
2 | Root.strings (Chinese, Simplified)
3 | Argus
4 |
5 | Created by WizJin on 2020/11/29.
6 |
7 | */
8 | "Version"="版本";
9 | "Acknowledgements"="版权声明";
10 |
--------------------------------------------------------------------------------
/Watch Extension/Assets.xcassets/Complication.complicationset/Modular.imageset/Modular-38mm@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wizjin/Argus/HEAD/Watch Extension/Assets.xcassets/Complication.complicationset/Modular.imageset/Modular-38mm@2x.png
--------------------------------------------------------------------------------
/Watch Extension/Assets.xcassets/Complication.complicationset/Modular.imageset/Modular-40mm@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wizjin/Argus/HEAD/Watch Extension/Assets.xcassets/Complication.complicationset/Modular.imageset/Modular-40mm@2x.png
--------------------------------------------------------------------------------
/Watch Extension/Assets.xcassets/Complication.complicationset/Modular.imageset/Modular-42mm@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wizjin/Argus/HEAD/Watch Extension/Assets.xcassets/Complication.complicationset/Modular.imageset/Modular-42mm@2x.png
--------------------------------------------------------------------------------
/Watch Extension/Assets.xcassets/Complication.complicationset/Modular.imageset/Modular-44mm@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wizjin/Argus/HEAD/Watch Extension/Assets.xcassets/Complication.complicationset/Modular.imageset/Modular-44mm@2x.png
--------------------------------------------------------------------------------
/Argus/Resources/Assets.xcassets/exclamationmark.circle.fill.imageset/exclamationmark.circle.fill.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wizjin/Argus/HEAD/Argus/Resources/Assets.xcassets/exclamationmark.circle.fill.imageset/exclamationmark.circle.fill.pdf
--------------------------------------------------------------------------------
/Argus/Resources/Assets.xcassets/rectangle.stack.badge.plus.imageset/rectangle.stack.badge.plus.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wizjin/Argus/HEAD/Argus/Resources/Assets.xcassets/rectangle.stack.badge.plus.imageset/rectangle.stack.badge.plus.pdf
--------------------------------------------------------------------------------
/Watch Extension/Assets.xcassets/Complication.complicationset/Circular.imageset/Circular-38mm@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wizjin/Argus/HEAD/Watch Extension/Assets.xcassets/Complication.complicationset/Circular.imageset/Circular-38mm@2x.png
--------------------------------------------------------------------------------
/Watch Extension/Assets.xcassets/Complication.complicationset/Circular.imageset/Circular-40mm@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wizjin/Argus/HEAD/Watch Extension/Assets.xcassets/Complication.complicationset/Circular.imageset/Circular-40mm@2x.png
--------------------------------------------------------------------------------
/Watch Extension/Assets.xcassets/Complication.complicationset/Circular.imageset/Circular-42mm@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wizjin/Argus/HEAD/Watch Extension/Assets.xcassets/Complication.complicationset/Circular.imageset/Circular-42mm@2x.png
--------------------------------------------------------------------------------
/Watch Extension/Assets.xcassets/Complication.complicationset/Circular.imageset/Circular-44mm@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wizjin/Argus/HEAD/Watch Extension/Assets.xcassets/Complication.complicationset/Circular.imageset/Circular-44mm@2x.png
--------------------------------------------------------------------------------
/Argus/Resources/Assets.xcassets/exclamationmark.triangle.fill.imageset/exclamationmark.triangle.fill.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wizjin/Argus/HEAD/Argus/Resources/Assets.xcassets/exclamationmark.triangle.fill.imageset/exclamationmark.triangle.fill.pdf
--------------------------------------------------------------------------------
/Watch Extension/Assets.xcassets/Complication.complicationset/Utilitarian.imageset/Utilitarian-38mm@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wizjin/Argus/HEAD/Watch Extension/Assets.xcassets/Complication.complicationset/Utilitarian.imageset/Utilitarian-38mm@2x.png
--------------------------------------------------------------------------------
/Watch Extension/Assets.xcassets/Complication.complicationset/Utilitarian.imageset/Utilitarian-40mm@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wizjin/Argus/HEAD/Watch Extension/Assets.xcassets/Complication.complicationset/Utilitarian.imageset/Utilitarian-40mm@2x.png
--------------------------------------------------------------------------------
/Watch Extension/Assets.xcassets/Complication.complicationset/Utilitarian.imageset/Utilitarian-42mm@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wizjin/Argus/HEAD/Watch Extension/Assets.xcassets/Complication.complicationset/Utilitarian.imageset/Utilitarian-42mm@2x.png
--------------------------------------------------------------------------------
/Watch Extension/Assets.xcassets/Complication.complicationset/Utilitarian.imageset/Utilitarian-44mm@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wizjin/Argus/HEAD/Watch Extension/Assets.xcassets/Complication.complicationset/Utilitarian.imageset/Utilitarian-44mm@2x.png
--------------------------------------------------------------------------------
/Argus/Views/Utils/AGViewController.h:
--------------------------------------------------------------------------------
1 | //
2 | // AGViewController.h
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/11/29.
6 | //
7 |
8 | #import
9 |
10 | @interface AGViewController : UIViewController
11 |
12 |
13 | @end
14 |
15 |
--------------------------------------------------------------------------------
/Watch Extension/Assets.xcassets/Complication.complicationset/Graphic Bezel.imageset/GraphicBezel-40mm@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wizjin/Argus/HEAD/Watch Extension/Assets.xcassets/Complication.complicationset/Graphic Bezel.imageset/GraphicBezel-40mm@2x.png
--------------------------------------------------------------------------------
/Watch Extension/Assets.xcassets/Complication.complicationset/Graphic Bezel.imageset/GraphicBezel-44mm@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wizjin/Argus/HEAD/Watch Extension/Assets.xcassets/Complication.complicationset/Graphic Bezel.imageset/GraphicBezel-44mm@2x.png
--------------------------------------------------------------------------------
/Watch Extension/Assets.xcassets/Complication.complicationset/Graphic Corner.imageset/GraphicCorner-40mm@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wizjin/Argus/HEAD/Watch Extension/Assets.xcassets/Complication.complicationset/Graphic Corner.imageset/GraphicCorner-40mm@2x.png
--------------------------------------------------------------------------------
/Watch Extension/Assets.xcassets/Complication.complicationset/Graphic Corner.imageset/GraphicCorner-44mm@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wizjin/Argus/HEAD/Watch Extension/Assets.xcassets/Complication.complicationset/Graphic Corner.imageset/GraphicCorner-44mm@2x.png
--------------------------------------------------------------------------------
/Watch Extension/Assets.xcassets/Complication.complicationset/Graphic Circular.imageset/GraphicCircular-40mm@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wizjin/Argus/HEAD/Watch Extension/Assets.xcassets/Complication.complicationset/Graphic Circular.imageset/GraphicCircular-40mm@2x.png
--------------------------------------------------------------------------------
/Watch Extension/Assets.xcassets/Complication.complicationset/Graphic Circular.imageset/GraphicCircular-44mm@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wizjin/Argus/HEAD/Watch Extension/Assets.xcassets/Complication.complicationset/Graphic Circular.imageset/GraphicCircular-44mm@2x.png
--------------------------------------------------------------------------------
/Watch Extension/Classes/ExtensionDelegate.h:
--------------------------------------------------------------------------------
1 | //
2 | // ExtensionDelegate.h
3 | // Watch Extension
4 | //
5 | // Created by WizJin on 2020/12/11.
6 | //
7 |
8 | #import
9 |
10 | @interface ExtensionDelegate : NSObject
11 |
12 | @end
13 |
--------------------------------------------------------------------------------
/Argus/Views/Main/AGMFATableView.h:
--------------------------------------------------------------------------------
1 | //
2 | // AGMFATableView.h
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/12/1.
6 | //
7 |
8 | #import
9 |
10 | NS_ASSUME_NONNULL_BEGIN
11 |
12 | @interface AGMFATableView : UITableView
13 |
14 | @end
15 |
16 | NS_ASSUME_NONNULL_END
17 |
--------------------------------------------------------------------------------
/Argus.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/Watch Extension/Classes/ComplicationController.h:
--------------------------------------------------------------------------------
1 | //
2 | // ComplicationController.h
3 | // Watch Extension
4 | //
5 | // Created by WizJin on 2020/12/11.
6 | //
7 |
8 | #import
9 |
10 | @interface ComplicationController : NSObject
11 |
12 | @end
13 |
--------------------------------------------------------------------------------
/Argus.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Watch Extension/Classes/InterfaceController.h:
--------------------------------------------------------------------------------
1 | //
2 | // InterfaceController.h
3 | // Watch Extension
4 | //
5 | // Created by WizJin on 2020/12/11.
6 | //
7 |
8 | #import
9 | #import
10 |
11 | @interface InterfaceController : WKInterfaceController
12 |
13 | @end
14 |
--------------------------------------------------------------------------------
/Argus/Classes/AppDelegate.h:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.h
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/11/29.
6 | //
7 |
8 | #import
9 |
10 | @interface AppDelegate : UIResponder
11 |
12 | @property (nullable, nonatomic, strong) UIWindow *window;
13 |
14 |
15 | @end
16 |
17 |
--------------------------------------------------------------------------------
/Argus/Views/Utils/AGBlurViewController.h:
--------------------------------------------------------------------------------
1 | //
2 | // AGBlurViewController.h
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/12/11.
6 | //
7 |
8 | #import
9 |
10 | NS_ASSUME_NONNULL_BEGIN
11 |
12 | @interface AGBlurViewController : UIViewController
13 |
14 | @end
15 |
16 | NS_ASSUME_NONNULL_END
17 |
--------------------------------------------------------------------------------
/Argus.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Argus/Views/Main/AGMainViewController.h:
--------------------------------------------------------------------------------
1 | //
2 | // AGMainViewController.h
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/11/30.
6 | //
7 |
8 | #import "AGViewController.h"
9 |
10 | NS_ASSUME_NONNULL_BEGIN
11 |
12 | @interface AGMainViewController : AGViewController
13 |
14 | @end
15 |
16 | NS_ASSUME_NONNULL_END
17 |
--------------------------------------------------------------------------------
/Argus/Views/Scan/AGScanViewController.h:
--------------------------------------------------------------------------------
1 | //
2 | // AGScanViewController.h
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/11/30.
6 | //
7 |
8 | #import "AGViewController.h"
9 |
10 | NS_ASSUME_NONNULL_BEGIN
11 |
12 | @interface AGScanViewController : AGViewController
13 |
14 | @end
15 |
16 | NS_ASSUME_NONNULL_END
17 |
--------------------------------------------------------------------------------
/Argus/Views/Export/AGExportViewController.h:
--------------------------------------------------------------------------------
1 | //
2 | // AGExportViewController.h
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/12/18.
6 | //
7 |
8 | #import "AGViewController.h"
9 |
10 | NS_ASSUME_NONNULL_BEGIN
11 |
12 | @interface AGExportViewController : AGViewController
13 |
14 | @end
15 |
16 | NS_ASSUME_NONNULL_END
17 |
--------------------------------------------------------------------------------
/Argus/Extensions/NSURL+AGExt.h:
--------------------------------------------------------------------------------
1 | //
2 | // NSURL+AGExt.h
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/12/16.
6 | //
7 |
8 | #import
9 |
10 | NS_ASSUME_NONNULL_BEGIN
11 |
12 | @interface NSURL (AGExt)
13 |
14 | - (nullable NSDate *)modificationDate;
15 |
16 |
17 | @end
18 |
19 | NS_ASSUME_NONNULL_END
20 |
--------------------------------------------------------------------------------
/Argus/Views/Export/AGExportTextViewController.h:
--------------------------------------------------------------------------------
1 | //
2 | // AGExportTextViewController.h
3 | // Argus
4 | //
5 | // Created by WizJin on 2021/3/20.
6 | //
7 |
8 | #import "AGViewController.h"
9 |
10 | NS_ASSUME_NONNULL_BEGIN
11 |
12 | @interface AGExportTextViewController : AGViewController
13 |
14 | @end
15 |
16 | NS_ASSUME_NONNULL_END
17 |
--------------------------------------------------------------------------------
/Argus/Extensions/UIColor+AGExt.h:
--------------------------------------------------------------------------------
1 | //
2 | // UIColor+AGExt.h
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/12/30.
6 | //
7 |
8 | #import
9 |
10 | NS_ASSUME_NONNULL_BEGIN
11 |
12 | @interface UIColor (AGExt)
13 |
14 | + (instancetype)colorWithRGBA:(uint32_t)rgba;
15 |
16 |
17 | @end
18 |
19 | NS_ASSUME_NONNULL_END
20 |
--------------------------------------------------------------------------------
/Argus/Views/Main/AGCreatedLabel.h:
--------------------------------------------------------------------------------
1 | //
2 | // AGCreatedLabel.h
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/12/8.
6 | //
7 |
8 | #import
9 |
10 | NS_ASSUME_NONNULL_BEGIN
11 |
12 | @interface AGCreatedLabel : UILabel
13 |
14 | @property (nonatomic, assign) uint64_t created;
15 |
16 |
17 | @end
18 |
19 | NS_ASSUME_NONNULL_END
20 |
--------------------------------------------------------------------------------
/Argus/Views/Settings/AGSettingsViewController.h:
--------------------------------------------------------------------------------
1 | //
2 | // AGSettingsViewController.h
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/11/30.
6 | //
7 |
8 | #import
9 |
10 | NS_ASSUME_NONNULL_BEGIN
11 |
12 | @interface AGSettingsViewController : XLFormViewController
13 |
14 | @end
15 |
16 | NS_ASSUME_NONNULL_END
17 |
--------------------------------------------------------------------------------
/Argus/Views/Editor/AGQRCodeView.h:
--------------------------------------------------------------------------------
1 | //
2 | // AGQRCodeView.h
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/12/8.
6 | //
7 |
8 | #import
9 |
10 | NS_ASSUME_NONNULL_BEGIN
11 |
12 | @interface AGQRCodeView : UIImageView
13 |
14 | @property (nonatomic, nullable, strong) NSString *url;
15 |
16 |
17 | @end
18 |
19 | NS_ASSUME_NONNULL_END
20 |
--------------------------------------------------------------------------------
/Argus/Extensions/UIViewController+AGExt.h:
--------------------------------------------------------------------------------
1 | //
2 | // UIViewController+AGExt.h
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/11/30.
6 | //
7 |
8 | #import
9 |
10 | NS_ASSUME_NONNULL_BEGIN
11 |
12 | @interface UIViewController (AGExt)
13 |
14 | - (UINavigationController *)navigation;
15 |
16 |
17 | @end
18 |
19 | NS_ASSUME_NONNULL_END
20 |
--------------------------------------------------------------------------------
/Argus/Views/Settings/AGAcknowledgementsViewController.h:
--------------------------------------------------------------------------------
1 | //
2 | // AGAcknowledgementsViewController.h
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/11/30.
6 | //
7 |
8 | #import "AGViewController.h"
9 |
10 | NS_ASSUME_NONNULL_BEGIN
11 |
12 | @interface AGAcknowledgementsViewController : AGViewController
13 |
14 | @end
15 |
16 | NS_ASSUME_NONNULL_END
17 |
--------------------------------------------------------------------------------
/Watch Extension/Classes/Theme.h:
--------------------------------------------------------------------------------
1 | //
2 | // Theme.h
3 | // Watch Extension
4 | //
5 | // Created by WizJin on 2020/12/13.
6 | //
7 |
8 | #import
9 | #import
10 |
11 | NS_ASSUME_NONNULL_BEGIN
12 |
13 | @interface Theme : NSObject
14 |
15 | + (UIColor *)tintColor;
16 |
17 |
18 | @end
19 |
20 | NS_ASSUME_NONNULL_END
21 |
--------------------------------------------------------------------------------
/Argus/Views/Main/AGMFAEmptyView.h:
--------------------------------------------------------------------------------
1 | //
2 | // AGMFAEmptyView.h
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/12/1.
6 | //
7 |
8 | #import
9 |
10 | NS_ASSUME_NONNULL_BEGIN
11 |
12 | @interface AGMFAEmptyView : UIScrollView
13 |
14 | - (instancetype)initWithTarget:(id)target action:(SEL)action;
15 |
16 |
17 | @end
18 |
19 | NS_ASSUME_NONNULL_END
20 |
--------------------------------------------------------------------------------
/Argus/Extensions/UIView+AGExt.h:
--------------------------------------------------------------------------------
1 | //
2 | // UIView+AGExt.h
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/12/1.
6 | //
7 |
8 | #import
9 |
10 | NS_ASSUME_NONNULL_BEGIN
11 |
12 | @interface UIView (AGExt)
13 |
14 | - (UIView *)findWithClassName:(NSString *)name;
15 | - (nullable UIImage *)snapshotImage;
16 |
17 |
18 | @end
19 |
20 | NS_ASSUME_NONNULL_END
21 |
--------------------------------------------------------------------------------
/Argus/Resources/Assets.xcassets/circle.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "circle.pdf",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | },
12 | "properties" : {
13 | "preserves-vector-representation" : true,
14 | "template-rendering-intent" : "template"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Argus/Resources/Assets.xcassets/gear.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "gear.pdf",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | },
12 | "properties" : {
13 | "preserves-vector-representation" : true,
14 | "template-rendering-intent" : "template"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Argus/Resources/Assets.xcassets/photo.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "photo.pdf",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | },
12 | "properties" : {
13 | "preserves-vector-representation" : true,
14 | "template-rendering-intent" : "template"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Argus/Resources/Assets.xcassets/qrcode.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "qrcode.pdf",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | },
12 | "properties" : {
13 | "preserves-vector-representation" : true,
14 | "template-rendering-intent" : "template"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Argus/Views/Utils/AGViewController.m:
--------------------------------------------------------------------------------
1 | //
2 | // AGViewController.m
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/11/29.
6 | //
7 |
8 | #import "AGViewController.h"
9 | #import "AGTheme.h"
10 |
11 | @implementation AGViewController
12 |
13 | - (void)viewDidLoad {
14 | [super viewDidLoad];
15 | self.view.backgroundColor = AGTheme.shared.backgroundColor;
16 | }
17 |
18 |
19 | @end
20 |
--------------------------------------------------------------------------------
/Argus/Resources/Assets.xcassets/lock.fill.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "lock.fill.pdf",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | },
12 | "properties" : {
13 | "preserves-vector-representation" : true,
14 | "template-rendering-intent" : "template"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Argus/Resources/Assets.xcassets/trash.fill.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "trash.fill.pdf",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | },
12 | "properties" : {
13 | "preserves-vector-representation" : true,
14 | "template-rendering-intent" : "template"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Argus/Extensions/UIButton+AGExt.h:
--------------------------------------------------------------------------------
1 | //
2 | // UIButton+AGExt.h
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/12/30.
6 | //
7 |
8 | #import
9 |
10 | NS_ASSUME_NONNULL_BEGIN
11 |
12 | @interface UIButton (AGExt)
13 |
14 | + (instancetype)buttonWithImage:(UIImage *)image target:(nullable id)target action:(nullable SEL)action;
15 |
16 |
17 | @end
18 |
19 | NS_ASSUME_NONNULL_END
20 |
--------------------------------------------------------------------------------
/Argus/Extensions/NSString+AGExt.h:
--------------------------------------------------------------------------------
1 | //
2 | // NSString+AGExt.h
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/11/29.
6 | //
7 |
8 | #import
9 |
10 | NS_ASSUME_NONNULL_BEGIN
11 |
12 | @interface NSString (AGExt)
13 |
14 | - (NSString *)localized;
15 | - (NSString *)formatSpace;
16 | - (NSString *)code;
17 | - (NSString *)trim;
18 |
19 |
20 | @end
21 |
22 | NS_ASSUME_NONNULL_END
23 |
--------------------------------------------------------------------------------
/Argus/Resources/Assets.xcassets/arrow.clockwise.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "arrow.clockwise.pdf",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | },
12 | "properties" : {
13 | "preserves-vector-representation" : true,
14 | "template-rendering-intent" : "template"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Argus/Resources/Assets.xcassets/bolt.slash.fill.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "bolt.slash.fill.pdf",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | },
12 | "properties" : {
13 | "preserves-vector-representation" : true,
14 | "template-rendering-intent" : "template"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Argus/Resources/Assets.xcassets/chevron.backward.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "chevron.backward.pdf",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | },
12 | "properties" : {
13 | "preserves-vector-representation" : true,
14 | "template-rendering-intent" : "template"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Argus/Resources/Assets.xcassets/qrcode.viewfinder.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "qrcode.viewfinder.pdf",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | },
12 | "properties" : {
13 | "preserves-vector-representation" : true,
14 | "template-rendering-intent" : "template"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Argus/Resources/Assets.xcassets/checkmark.circle.fill.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "checkmark.circle.fill.pdf",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | },
12 | "properties" : {
13 | "preserves-vector-representation" : true,
14 | "template-rendering-intent" : "template"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Argus/Resources/Assets.xcassets/square.and.arrow.up.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "square.and.arrow.up.pdf",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | },
12 | "properties" : {
13 | "preserves-vector-representation" : true,
14 | "template-rendering-intent" : "template"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Argus/Views/Editor/AGEditorViewController.h:
--------------------------------------------------------------------------------
1 | //
2 | // AGEditorViewController.h
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/11/30.
6 | //
7 |
8 | #import "AGViewController.h"
9 | #import "AGMFAModel.h"
10 |
11 | NS_ASSUME_NONNULL_BEGIN
12 |
13 | @interface AGEditorViewController : AGViewController
14 |
15 | - (instancetype)initWithModel:(AGMFAModel *)model;
16 |
17 |
18 | @end
19 |
20 | NS_ASSUME_NONNULL_END
21 |
--------------------------------------------------------------------------------
/Argus/Views/Export/AGExportQrcodeViewController.h:
--------------------------------------------------------------------------------
1 | //
2 | // AGExportQrcodeViewController.h
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/12/18.
6 | //
7 |
8 | #import "AGViewController.h"
9 |
10 | NS_ASSUME_NONNULL_BEGIN
11 |
12 | @interface AGExportQrcodeViewController : AGViewController
13 |
14 | - (instancetype)initWithParameters:(NSDictionary *)params;
15 |
16 |
17 | @end
18 |
19 | NS_ASSUME_NONNULL_END
20 |
--------------------------------------------------------------------------------
/Argus/Resources/Assets.xcassets/chevron.left.circle.fill.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "chevron.left.circle.fill.pdf",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | },
12 | "properties" : {
13 | "preserves-vector-representation" : true,
14 | "template-rendering-intent" : "template"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Argus/Resources/Assets.xcassets/rectangle.stack.badge.plus.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "rectangle.stack.badge.plus.pdf",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | },
12 | "properties" : {
13 | "preserves-vector-representation" : true,
14 | "template-rendering-intent" : "template"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Argus/Services/AGSecurity.h:
--------------------------------------------------------------------------------
1 | //
2 | // AGSecurity.h
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/12/10.
6 | //
7 |
8 | #import
9 |
10 | NS_ASSUME_NONNULL_BEGIN
11 |
12 | @interface AGSecurity : NSObject
13 |
14 | @property (nonatomic, assign) BOOL hasLocker;
15 |
16 | + (instancetype)shared;
17 | - (BOOL)checkLocker;
18 | - (BOOL)isLocking;
19 |
20 |
21 | @end
22 |
23 | NS_ASSUME_NONNULL_END
24 |
--------------------------------------------------------------------------------
/Argus/Views/Web/AGWebViewRefresher.h:
--------------------------------------------------------------------------------
1 | //
2 | // AGWebViewRefresher.h
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/11/30.
6 | //
7 |
8 | #import
9 |
10 | NS_ASSUME_NONNULL_BEGIN
11 |
12 | @interface AGWebViewRefresher : UIRefreshControl
13 |
14 | @property (nonatomic, strong) NSString *host;
15 | @property (nonatomic, assign) BOOL hasOnlySecureContent;
16 |
17 |
18 | @end
19 |
20 | NS_ASSUME_NONNULL_END
21 |
--------------------------------------------------------------------------------
/Argus/Resources/Assets.xcassets/exclamationmark.circle.fill.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "exclamationmark.circle.fill.pdf",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | },
12 | "properties" : {
13 | "preserves-vector-representation" : true,
14 | "template-rendering-intent" : "template"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Argus/Views/Utils/AGBlurViewController.m:
--------------------------------------------------------------------------------
1 | //
2 | // AGBlurViewController.m
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/12/11.
6 | //
7 |
8 | #import "AGBlurViewController.h"
9 |
10 | @implementation AGBlurViewController
11 |
12 | - (void)viewDidLoad {
13 | [super viewDidLoad];
14 | self.view = [[UIVisualEffectView alloc] initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]];
15 | }
16 |
17 |
18 | @end
19 |
--------------------------------------------------------------------------------
/Argus/Resources/Assets.xcassets/exclamationmark.triangle.fill.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "exclamationmark.triangle.fill.pdf",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | },
12 | "properties" : {
13 | "preserves-vector-representation" : true,
14 | "template-rendering-intent" : "template"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Argus/Resources/zh-Hans.lproj/InfoPlist.strings:
--------------------------------------------------------------------------------
1 | /*
2 | InfoPlist.strings (Chinese, Simplified)
3 | Argus
4 |
5 | Created by WizJin on 2020/11/29.
6 |
7 | */
8 | CFBundleDisplayName="Argus";
9 | NSCameraUsageDescription="在您需要时,用来扫描二维码。";
10 | NSPhotoLibraryUsageDescription="在您需要时,从相册中获取二维码。";
11 | NSPhotoLibraryAddUsageDescription="在您需要时,将导出的二维码保存到照片库中。";
12 | NSFaceIDUsageDescription="在您需要时,使用面容ID解锁应用。";
13 | ScanActionTitle="扫一扫";
14 |
--------------------------------------------------------------------------------
/Watch Extension/Assets.xcassets/Complication.complicationset/Graphic Large Rectangular.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "watch",
5 | "scale" : "2x",
6 | "screen-width" : ">161"
7 | },
8 | {
9 | "idiom" : "watch",
10 | "scale" : "2x",
11 | "screen-width" : ">183"
12 | }
13 | ],
14 | "info" : {
15 | "author" : "xcode",
16 | "version" : 1
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/Argus/Extensions/UIColor+AGExt.m:
--------------------------------------------------------------------------------
1 | //
2 | // UIColor+AGExt.m
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/12/30.
6 | //
7 |
8 | #import "UIColor+AGExt.h"
9 |
10 | @implementation UIColor (AGExt)
11 |
12 | + (instancetype)colorWithRGBA:(uint32_t)rgba {
13 | return [UIColor colorWithRed:((rgba >> 24)&0x00ff)/255.0 green:((rgba >> 16)&0x00ff)/255.0 blue:((rgba >> 8)&0x00ff)/255.0 alpha:(rgba&0x00ff)/255.0];
14 | }
15 |
16 |
17 | @end
18 |
--------------------------------------------------------------------------------
/Argus/Views/Main/AGCountdownView.h:
--------------------------------------------------------------------------------
1 | //
2 | // AGCountdownView.h
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/11/30.
6 | //
7 |
8 | #import
9 | #import "AGMFAModel.h"
10 |
11 | NS_ASSUME_NONNULL_BEGIN
12 |
13 | @interface AGCountdownView : UIView
14 |
15 | @property (nonatomic, strong) UIColor *tintColor;
16 |
17 | - (void)update:(AGMFAModel *)model remainder:(uint64_t)r;
18 |
19 |
20 | @end
21 |
22 | NS_ASSUME_NONNULL_END
23 |
--------------------------------------------------------------------------------
/Argus/Views/Main/AGCodeView.h:
--------------------------------------------------------------------------------
1 | //
2 | // AGCodeView.h
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/12/8.
6 | //
7 |
8 | #import
9 | #import "AGMFAModel.h"
10 |
11 | NS_ASSUME_NONNULL_BEGIN
12 |
13 | @interface AGCodeView : UILabel
14 |
15 | @property (nonatomic, assign) CGFloat fontSize;
16 |
17 | - (void)reset;
18 | - (uint64_t)update:(AGMFAModel *)model now:(time_t)now;
19 |
20 |
21 | @end
22 |
23 | NS_ASSUME_NONNULL_END
24 |
--------------------------------------------------------------------------------
/Watch Extension/Classes/Theme.m:
--------------------------------------------------------------------------------
1 | //
2 | // Theme.m
3 | // Watch Extension
4 | //
5 | // Created by WizJin on 2020/12/13.
6 | //
7 |
8 | #import "Theme.h"
9 |
10 | @implementation Theme
11 |
12 | + (UIColor *)tintColor {
13 | static UIColor *color = nil;
14 | static dispatch_once_t onceToken;
15 | dispatch_once(&onceToken, ^{
16 | color = [UIColor colorNamed:@"AccentColor"];
17 | });
18 | return color;
19 | }
20 |
21 |
22 | @end
23 |
--------------------------------------------------------------------------------
/Argus/Views/Export/AGExportTextViewController.m:
--------------------------------------------------------------------------------
1 | //
2 | // AGExportTextViewController.m
3 | // Argus
4 | //
5 | // Created by WizJin on 2021/3/20.
6 | //
7 |
8 | #import "AGExportTextViewController.h"
9 |
10 | @interface AGExportTextViewController ()
11 |
12 | @end
13 |
14 | @implementation AGExportTextViewController
15 |
16 | - (void)viewDidLoad {
17 | [super viewDidLoad];
18 | self.title = @"Export into text file".localized;
19 | }
20 |
21 |
22 | @end
23 |
--------------------------------------------------------------------------------
/Argus/Resources/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0xFF",
9 | "green" : "0x84",
10 | "red" : "0x0A"
11 | }
12 | },
13 | "idiom" : "universal"
14 | }
15 | ],
16 | "info" : {
17 | "author" : "xcode",
18 | "version" : 1
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Argus/Views/Web/AGWebViewController.h:
--------------------------------------------------------------------------------
1 | //
2 | // AGWebViewController.h
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/11/30.
6 | //
7 |
8 | #import "AGViewController.h"
9 |
10 | NS_ASSUME_NONNULL_BEGIN
11 |
12 | @interface AGWebViewController : AGViewController
13 |
14 | @property (nonatomic, readonly, strong) NSURL *url;
15 |
16 | - (instancetype)initWithURL:(NSURL *)url withParams:(NSDictionary *)params;
17 |
18 |
19 | @end
20 |
21 | NS_ASSUME_NONNULL_END
22 |
--------------------------------------------------------------------------------
/Argus/Extensions/UIImage+AGExt.h:
--------------------------------------------------------------------------------
1 | //
2 | // UIImage+AGExt.h
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/12/30.
6 | //
7 |
8 | #import
9 |
10 | NS_ASSUME_NONNULL_BEGIN
11 |
12 | @interface UIImage (AGExt)
13 |
14 | + (instancetype)imageWithSymbol:(NSString *)name;
15 | + (instancetype)imageWithSymbol:(NSString *)name height:(CGFloat)height;
16 | - (instancetype)resizeWithHeight:(CGFloat)height;
17 | - (instancetype)barItemImage;
18 |
19 |
20 | @end
21 |
22 | NS_ASSUME_NONNULL_END
23 |
--------------------------------------------------------------------------------
/Watch/zh-Hans.lproj/Interface.strings:
--------------------------------------------------------------------------------
1 |
2 | /* Class = "WKInterfaceLabel"; text = "Title"; ObjectID = "4bM-SJ-4gD"; */
3 | "4bM-SJ-4gD.text" = "Title";
4 |
5 | /* Class = "WKInterfaceLabel"; text = "NoMFA"; ObjectID = "5Qe-sf-A8T"; */
6 | "5Qe-sf-A8T.text" = "NoMFA";
7 |
8 | /* Class = "WKInterfaceLabel"; text = "Account"; ObjectID = "dpi-J4-hIV"; */
9 | "dpi-J4-hIV.text" = "Account";
10 |
11 | /* Class = "WKInterfaceLabel"; text = "--- --- --"; ObjectID = "nmj-Tr-gbG"; */
12 | "nmj-Tr-gbG.text" = "--- --- --";
13 |
--------------------------------------------------------------------------------
/Argus/Extensions/NSData+AGExt.h:
--------------------------------------------------------------------------------
1 | //
2 | // NSData+AGExt.h
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/11/30.
6 | //
7 |
8 | #import
9 |
10 | NS_ASSUME_NONNULL_BEGIN
11 |
12 | @interface NSData (AGExt)
13 |
14 | + (nullable instancetype)dataWithBase32EncodedString:(NSString *)base32String;
15 | - (NSString *)base32EncodedString;
16 | - (NSData *)sha1;
17 | - (NSString *)hex;
18 | - (NSData *)compress;
19 | - (NSData *)decompress;
20 |
21 |
22 | @end
23 |
24 | NS_ASSUME_NONNULL_END
25 |
--------------------------------------------------------------------------------
/Argus/Classes/main.m:
--------------------------------------------------------------------------------
1 | //
2 | // main.m
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/11/29.
6 | //
7 |
8 | #import
9 | #import "AppDelegate.h"
10 |
11 | int main(int argc, char * argv[]) {
12 | NSString * appDelegateClassName;
13 | @autoreleasepool {
14 | // Setup code that might create autoreleased objects goes here.
15 | appDelegateClassName = NSStringFromClass([AppDelegate class]);
16 | }
17 | return UIApplicationMain(argc, argv, nil, appDelegateClassName);
18 | }
19 |
--------------------------------------------------------------------------------
/Argus/Models/AGMFAModel+GPB.h:
--------------------------------------------------------------------------------
1 | //
2 | // AGMFAModel+GPB.h
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/12/18.
6 | //
7 |
8 | #import "AGMFAModel.h"
9 |
10 | NS_ASSUME_NONNULL_BEGIN
11 |
12 | @class AGMOtpParameters;
13 |
14 | @interface AGMFAModel (GPB)
15 |
16 | + (nullable NSString *)URLWithParams:(AGMOtpParameters *)params API_UNAVAILABLE(watchos);
17 | - (BOOL)calcCanExportPB API_UNAVAILABLE(watchos);
18 | - (AGMOtpParameters *)pbParams API_UNAVAILABLE(watchos);
19 |
20 |
21 | @end
22 |
23 | NS_ASSUME_NONNULL_END
24 |
--------------------------------------------------------------------------------
/Argus/Views/Utils/AGTableViewCell.h:
--------------------------------------------------------------------------------
1 | //
2 | // AGTableViewCell.h
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/12/18.
6 | //
7 |
8 | #import
9 |
10 | NS_ASSUME_NONNULL_BEGIN
11 |
12 | #define kAGTableCellMargin 8
13 |
14 | @interface AGTableViewCell : UITableViewCell
15 |
16 | @property (nonatomic, readonly, strong) UIView *mainView;
17 |
18 | - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(nullable NSString *)reuseIdentifier;
19 |
20 |
21 | @end
22 |
23 | NS_ASSUME_NONNULL_END
24 |
--------------------------------------------------------------------------------
/Argus/Resources/Base.lproj/InfoPlist.strings:
--------------------------------------------------------------------------------
1 | /*
2 | InfoPlist.strings (Base)
3 | Argus
4 |
5 | Created by WizJin on 2020/11/29.
6 |
7 | */
8 | CFBundleDisplayName="Argus";
9 | NSCameraUsageDescription="To scan a QR code when you need it.";
10 | NSPhotoLibraryUsageDescription="To picker a QR code from photo when you need it.";
11 | NSPhotoLibraryAddUsageDescription="To save export QR code into photo library when you need it.";
12 | NSFaceIDUsageDescription="Use FaceID to unlock app when you need it.";
13 | ScanActionTitle="Scan QR code";
14 |
--------------------------------------------------------------------------------
/Argus/Views/Export/AGExportTableViewCell.h:
--------------------------------------------------------------------------------
1 | //
2 | // AGExportTableViewCell.h
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/12/18.
6 | //
7 |
8 | #import
9 | #import "AGTableViewCell.h"
10 | #import "AGMFAModel.h"
11 |
12 | NS_ASSUME_NONNULL_BEGIN
13 |
14 | #define kAGExportCellHeight 80
15 |
16 | @interface AGExportTableViewCell : AGTableViewCell
17 |
18 | @property (nonatomic, nullable, strong) AGMFAModel *model;
19 |
20 | - (void)setModel:(AGMFAModel *)model;
21 |
22 |
23 | @end
24 |
25 | NS_ASSUME_NONNULL_END
26 |
--------------------------------------------------------------------------------
/Argus/Extensions/UIBarButtonItem+AGExt.h:
--------------------------------------------------------------------------------
1 | //
2 | // UIBarButtonItem+AGExt.h
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/12/30.
6 | //
7 |
8 | #import
9 |
10 | NS_ASSUME_NONNULL_BEGIN
11 |
12 | @interface UIBarButtonItem (AGExt)
13 |
14 | + (instancetype)itemWithTitle:(nullable NSString *)title target:(nullable id)target action:(nullable SEL)action;
15 | + (instancetype)itemWithImage:(nullable UIImage *)image target:(nullable id)target action:(nullable SEL)action;
16 |
17 |
18 | @end
19 |
20 | NS_ASSUME_NONNULL_END
21 |
--------------------------------------------------------------------------------
/Argus/Extensions/NSURL+AGExt.m:
--------------------------------------------------------------------------------
1 | //
2 | // NSURL+AGExt.m
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/12/16.
6 | //
7 |
8 | #import "NSURL+AGExt.h"
9 |
10 | @implementation NSURL (AGExt)
11 |
12 | - (nullable NSDate *)modificationDate {
13 | NSDate *date = nil;
14 | if (self.isFileURL) {
15 | NSError *error = nil;
16 | [self getResourceValue:&date forKey:NSURLContentModificationDateKey error:&error];
17 | if (error != nil) {
18 | date = nil;
19 | }
20 | }
21 | return date;
22 | }
23 |
24 |
25 | @end
26 |
--------------------------------------------------------------------------------
/Argus/Models/AGFile.h:
--------------------------------------------------------------------------------
1 | //
2 | // AGFile.h
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/12/16.
6 | //
7 |
8 | #import
9 |
10 | NS_ASSUME_NONNULL_BEGIN
11 |
12 | @interface AGFile : NSObject
13 |
14 | @property (nonatomic, readonly, strong) NSURL *pathURL;
15 | @property (nonatomic, readonly, strong) NSString *dataKey;
16 |
17 | - (instancetype)initWithURL:(NSURL *)url;
18 | - (BOOL)changed;
19 | - (NSData *)fileData;
20 | - (BOOL)write:(NSData *)data updateStatus:(BOOL)update;
21 |
22 |
23 | @end
24 |
25 | NS_ASSUME_NONNULL_END
26 |
--------------------------------------------------------------------------------
/Argus/Services/AGDevice.h:
--------------------------------------------------------------------------------
1 | //
2 | // AGDevice.h
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/11/30.
6 | //
7 |
8 | #import
9 |
10 | NS_ASSUME_NONNULL_BEGIN
11 |
12 | @interface AGDevice : NSObject
13 |
14 | @property (nonatomic, readonly, strong) NSString *name;
15 | @property (nonatomic, readonly, strong) NSString *version;
16 | @property (nonatomic, readonly, assign) uint32_t build;
17 | @property (nonatomic, readonly, strong) NSURL *docdir;
18 |
19 | + (instancetype)shared;
20 |
21 |
22 | @end
23 |
24 | NS_ASSUME_NONNULL_END
25 |
--------------------------------------------------------------------------------
/Argus/Extensions/UIViewController+AGExt.m:
--------------------------------------------------------------------------------
1 | //
2 | // UIViewController+AGExt.m
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/11/30.
6 | //
7 |
8 | #import "UIViewController+AGExt.h"
9 |
10 | @implementation UIViewController (AGExt)
11 |
12 | - (UINavigationController *)navigation {
13 | UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:self];
14 | if (@available(iOS 13.0, *)) {
15 | navigationController.modalPresentationStyle = UIModalPresentationPopover;
16 | }
17 | return navigationController;
18 | }
19 |
20 |
21 | @end
22 |
--------------------------------------------------------------------------------
/Watch Extension/Assets.xcassets/Complication.complicationset/Extra Large.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "watch",
5 | "scale" : "2x",
6 | "screen-width" : "<=145"
7 | },
8 | {
9 | "idiom" : "watch",
10 | "scale" : "2x",
11 | "screen-width" : ">161"
12 | },
13 | {
14 | "idiom" : "watch",
15 | "scale" : "2x",
16 | "screen-width" : ">145"
17 | },
18 | {
19 | "idiom" : "watch",
20 | "scale" : "2x",
21 | "screen-width" : ">183"
22 | }
23 | ],
24 | "info" : {
25 | "author" : "xcode",
26 | "version" : 1
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Watch Extension/Assets.xcassets/Complication.complicationset/Graphic Extra Large.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "watch",
5 | "scale" : "2x",
6 | "screen-width" : "<=145"
7 | },
8 | {
9 | "idiom" : "watch",
10 | "scale" : "2x",
11 | "screen-width" : ">161"
12 | },
13 | {
14 | "idiom" : "watch",
15 | "scale" : "2x",
16 | "screen-width" : ">145"
17 | },
18 | {
19 | "idiom" : "watch",
20 | "scale" : "2x",
21 | "screen-width" : ">183"
22 | }
23 | ],
24 | "info" : {
25 | "author" : "xcode",
26 | "version" : 1
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Argus/Models/AGMFAStorage.h:
--------------------------------------------------------------------------------
1 | //
2 | // AGMFAStorage.h
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/12/12.
6 | //
7 |
8 | #import
9 | #import "AGFile.h"
10 | #import "AGMFAModel.h"
11 |
12 | NS_ASSUME_NONNULL_BEGIN
13 |
14 | @interface AGMFAStorage : AGFile
15 |
16 | - (instancetype)initWithURL:(NSURL *)url;
17 | - (BOOL)containsItem:(AGMFAModel *)item;
18 | - (void)removeItem:(AGMFAModel *)item;
19 | - (void)addItem:(AGMFAModel *)item;
20 | - (AGMFAModel *)itemAtIndex:(NSInteger)index;
21 | - (NSUInteger)count;
22 | - (BOOL)load;
23 | - (BOOL)save;
24 | - (BOOL)saveData:(NSData *)data;
25 |
26 |
27 | @end
28 |
29 | NS_ASSUME_NONNULL_END
30 |
--------------------------------------------------------------------------------
/Podfile.lock:
--------------------------------------------------------------------------------
1 | PODS:
2 | - JLRoutes (2.1)
3 | - Masonry (1.1.0)
4 | - Protobuf (3.18.0)
5 | - XLForm (4.3.0)
6 |
7 | DEPENDENCIES:
8 | - JLRoutes
9 | - Masonry
10 | - Protobuf
11 | - XLForm
12 |
13 | SPEC REPOS:
14 | https://github.com/CocoaPods/Specs.git:
15 | - JLRoutes
16 | - Masonry
17 | - Protobuf
18 | - XLForm
19 |
20 | SPEC CHECKSUMS:
21 | JLRoutes: d755245322b94227662ea3e43492fdca94e05c5b
22 | Masonry: 678fab65091a9290e40e2832a55e7ab731aad201
23 | Protobuf: 1a37ebea1338949e9ac35a3f06e80b3f536eec8d
24 | XLForm: a439f9512078ed538e8335c8b06d3f3286bbfd37
25 |
26 | PODFILE CHECKSUM: 373eb6a5671674a47f9ada5d84a74bf1f49a5caa
27 |
28 | COCOAPODS: 1.10.0
29 |
--------------------------------------------------------------------------------
/Argus/Argus.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.developer.default-data-protection
6 | NSFileProtectionComplete
7 | com.apple.developer.icloud-container-identifiers
8 |
9 | iCloud.com.wizjin.argus
10 |
11 | com.apple.developer.icloud-services
12 |
13 | CloudDocuments
14 |
15 | com.apple.developer.ubiquity-container-identifiers
16 |
17 | iCloud.com.wizjin.argus
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/Watch Extension/Assets.xcassets/Complication.complicationset/Graphic Bezel.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "watch",
5 | "scale" : "2x",
6 | "screen-width" : "<=145"
7 | },
8 | {
9 | "filename" : "GraphicBezel-40mm@2x.png",
10 | "idiom" : "watch",
11 | "scale" : "2x",
12 | "screen-width" : ">161"
13 | },
14 | {
15 | "idiom" : "watch",
16 | "scale" : "2x",
17 | "screen-width" : ">145"
18 | },
19 | {
20 | "filename" : "GraphicBezel-44mm@2x.png",
21 | "idiom" : "watch",
22 | "scale" : "2x",
23 | "screen-width" : ">183"
24 | }
25 | ],
26 | "info" : {
27 | "author" : "xcode",
28 | "version" : 1
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/Watch Extension/Assets.xcassets/Complication.complicationset/Graphic Corner.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "watch",
5 | "scale" : "2x",
6 | "screen-width" : "<=145"
7 | },
8 | {
9 | "filename" : "GraphicCorner-40mm@2x.png",
10 | "idiom" : "watch",
11 | "scale" : "2x",
12 | "screen-width" : ">161"
13 | },
14 | {
15 | "idiom" : "watch",
16 | "scale" : "2x",
17 | "screen-width" : ">145"
18 | },
19 | {
20 | "filename" : "GraphicCorner-44mm@2x.png",
21 | "idiom" : "watch",
22 | "scale" : "2x",
23 | "screen-width" : ">183"
24 | }
25 | ],
26 | "info" : {
27 | "author" : "xcode",
28 | "version" : 1
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/Watch Extension/Classes/CodeRowType.h:
--------------------------------------------------------------------------------
1 | //
2 | // CodeRowType.h
3 | // Watch Extension
4 | //
5 | // Created by WizJin on 2020/12/11.
6 | //
7 |
8 | #import
9 | #import
10 | #import "AGMFAModel.h"
11 |
12 | NS_ASSUME_NONNULL_BEGIN
13 |
14 | @interface CodeRowType : NSObject
15 |
16 | @property (nonatomic, weak) AGMFAModel *model;
17 |
18 | @property (weak, nonatomic) IBOutlet WKInterfaceLabel *titleLabel;
19 | @property (weak, nonatomic) IBOutlet WKInterfaceLabel *codeLabel;
20 | @property (weak, nonatomic) IBOutlet WKInterfaceLabel *accountLabel;
21 | @property (weak, nonatomic) IBOutlet WKInterfaceLabel *timerLabel;
22 |
23 | - (void)update:(time_t)now;
24 |
25 |
26 | @end
27 |
28 | NS_ASSUME_NONNULL_END
29 |
--------------------------------------------------------------------------------
/Argus/Views/Main/AGMFATableViewCell.h:
--------------------------------------------------------------------------------
1 | //
2 | // AGMFATableViewCell.h
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/11/30.
6 | //
7 |
8 | #import
9 | #import "AGTableViewCell.h"
10 | #import "AGMFAModel.h"
11 |
12 | NS_ASSUME_NONNULL_BEGIN
13 |
14 | #define kAGMFACellHeight 120
15 |
16 | @interface AGMFATableViewCell : AGTableViewCell
17 |
18 | @property (nonatomic, nullable, strong) AGMFAModel *model;
19 |
20 | + (UIContextualAction *)actionEdit:(UITableView *)tableView indexPath:(NSIndexPath *)indexPath;
21 | + (UIContextualAction *)actionDelete:(UITableView *)tableView indexPath:(NSIndexPath *)indexPath;
22 | - (void)setModel:(AGMFAModel *)model;
23 | - (void)update:(time_t)now;
24 |
25 |
26 | @end
27 |
28 | NS_ASSUME_NONNULL_END
29 |
--------------------------------------------------------------------------------
/Argus/Extensions/UIBarButtonItem+AGExt.m:
--------------------------------------------------------------------------------
1 | //
2 | // UIBarButtonItem+AGExt.m
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/12/30.
6 | //
7 |
8 | #import "UIBarButtonItem+AGExt.h"
9 |
10 | @implementation UIBarButtonItem (AGExt)
11 |
12 | + (instancetype)itemWithTitle:(nullable NSString *)title target:(nullable id)target action:(nullable SEL)action {
13 | return [[UIBarButtonItem alloc] initWithTitle:title style:UIBarButtonItemStylePlain target:target action:action];
14 | }
15 |
16 | + (instancetype)itemWithImage:(nullable UIImage *)image target:(nullable id)target action:(nullable SEL)action {
17 | return [[UIBarButtonItem alloc] initWithImage:image.barItemImage style:UIBarButtonItemStylePlain target:target action:action];
18 | }
19 |
20 |
21 | @end
22 |
--------------------------------------------------------------------------------
/Watch Extension/Assets.xcassets/Complication.complicationset/Graphic Circular.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "watch",
5 | "scale" : "2x",
6 | "screen-width" : "<=145"
7 | },
8 | {
9 | "filename" : "GraphicCircular-40mm@2x.png",
10 | "idiom" : "watch",
11 | "scale" : "2x",
12 | "screen-width" : ">161"
13 | },
14 | {
15 | "idiom" : "watch",
16 | "scale" : "2x",
17 | "screen-width" : ">145"
18 | },
19 | {
20 | "filename" : "GraphicCircular-44mm@2x.png",
21 | "idiom" : "watch",
22 | "scale" : "2x",
23 | "screen-width" : ">183"
24 | }
25 | ],
26 | "info" : {
27 | "author" : "xcode",
28 | "version" : 1
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/Watch Extension/Assets.xcassets/Complication.complicationset/Modular.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "Modular-38mm@2x.png",
5 | "idiom" : "watch",
6 | "scale" : "2x",
7 | "screen-width" : "<=145"
8 | },
9 | {
10 | "filename" : "Modular-40mm@2x.png",
11 | "idiom" : "watch",
12 | "scale" : "2x",
13 | "screen-width" : ">161"
14 | },
15 | {
16 | "filename" : "Modular-42mm@2x.png",
17 | "idiom" : "watch",
18 | "scale" : "2x",
19 | "screen-width" : ">145"
20 | },
21 | {
22 | "filename" : "Modular-44mm@2x.png",
23 | "idiom" : "watch",
24 | "scale" : "2x",
25 | "screen-width" : ">183"
26 | }
27 | ],
28 | "info" : {
29 | "author" : "xcode",
30 | "version" : 1
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/Watch Extension/Assets.xcassets/Complication.complicationset/Utilitarian.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "Utilitarian-38mm@2x.png",
5 | "idiom" : "watch",
6 | "scale" : "2x",
7 | "screen-width" : "<=145"
8 | },
9 | {
10 | "filename" : "Utilitarian-40mm@2x.png",
11 | "idiom" : "watch",
12 | "scale" : "2x",
13 | "screen-width" : ">161"
14 | },
15 | {
16 | "filename" : "Utilitarian-42mm@2x.png",
17 | "idiom" : "watch",
18 | "scale" : "2x",
19 | "screen-width" : ">145"
20 | },
21 | {
22 | "filename" : "Utilitarian-44mm@2x.png",
23 | "idiom" : "watch",
24 | "scale" : "2x",
25 | "screen-width" : ">183"
26 | }
27 | ],
28 | "info" : {
29 | "author" : "xcode",
30 | "version" : 1
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/Argus/Services/AGRouter.h:
--------------------------------------------------------------------------------
1 | //
2 | // AGRouter.h
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/11/30.
6 | //
7 |
8 | #import
9 |
10 | NS_ASSUME_NONNULL_BEGIN
11 |
12 | @interface AGRouter : NSObject
13 |
14 | @property (nonatomic, readonly, strong) UIWindow *window;
15 |
16 | + (instancetype)shared;
17 | - (void)active;
18 | - (void)deactive;
19 | - (BOOL)launchWithOptions:(NSDictionary *)options;
20 | - (BOOL)handleURL:(NSURL *)url;
21 | - (BOOL)handleShortcut:(NSString *)url;
22 | - (BOOL)routeTo:(NSString *)url;
23 | - (BOOL)routeTo:(NSString *)url withParams:(nullable NSDictionary *)params;
24 | - (void)showViewController:(UIViewController *)vc animated:(BOOL)animated;
25 | - (void)presentViewController:(UIViewController *)vc animated:(BOOL)animated;
26 | - (void)makeToast:(NSString *)message;
27 |
28 |
29 | @end
30 |
31 | NS_ASSUME_NONNULL_END
32 |
--------------------------------------------------------------------------------
/Argus/Resources/Assets.xcassets/CellColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0xFF",
9 | "green" : "0xFF",
10 | "red" : "0xFF"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "srgb",
24 | "components" : {
25 | "alpha" : "1.000",
26 | "blue" : "0x1E",
27 | "green" : "0x1C",
28 | "red" : "0x1C"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Watch Extension/Assets.xcassets/Complication.complicationset/Circular.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "Circular-38mm@2x.png",
5 | "idiom" : "watch",
6 | "scale" : "2x",
7 | "screen-width" : "<=145"
8 | },
9 | {
10 | "filename" : "Circular-40mm@2x.png",
11 | "idiom" : "watch",
12 | "scale" : "2x",
13 | "screen-width" : ">161"
14 | },
15 | {
16 | "filename" : "Circular-42mm@2x.png",
17 | "idiom" : "watch",
18 | "scale" : "2x",
19 | "screen-width" : ">145"
20 | },
21 | {
22 | "filename" : "Circular-44mm@2x.png",
23 | "idiom" : "watch",
24 | "scale" : "2x",
25 | "screen-width" : ">183"
26 | }
27 | ],
28 | "info" : {
29 | "author" : "xcode",
30 | "version" : 1
31 | },
32 | "properties" : {
33 | "template-rendering-intent" : "template"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Argus/Extensions/UIView+AGExt.m:
--------------------------------------------------------------------------------
1 | //
2 | // UIView+AGExt.m
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/12/1.
6 | //
7 |
8 | #import "UIView+AGExt.h"
9 |
10 | @implementation UIView (AGExt)
11 |
12 | - (UIView *)findWithClassName:(NSString *)name {
13 | if (name.length > 0) {
14 | for (UIView *subview in self.subviews) {
15 | if ([NSStringFromClass([subview class]) isEqualToString:name]) {
16 | return subview;
17 | }
18 | }
19 | }
20 | return nil;
21 | }
22 |
23 | - (nullable UIImage *)snapshotImage {
24 | UIImage *image = nil;
25 | if (self != nil) {
26 | UIGraphicsBeginImageContextWithOptions(self.bounds.size, NO, UIScreen.mainScreen.scale);
27 | [self.layer renderInContext:UIGraphicsGetCurrentContext()];
28 | image = UIGraphicsGetImageFromCurrentImageContext();
29 | UIGraphicsEndImageContext();
30 | }
31 | return image;
32 | }
33 |
34 |
35 | @end
36 |
--------------------------------------------------------------------------------
/Argus/Services/AGDevice.m:
--------------------------------------------------------------------------------
1 | //
2 | // AGDevice.m
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/11/30.
6 | //
7 |
8 | #import "AGDevice.h"
9 |
10 | @implementation AGDevice
11 |
12 | + (instancetype)shared {
13 | static AGDevice *device;
14 | static dispatch_once_t onceToken;
15 | dispatch_once(&onceToken, ^{
16 | device = [AGDevice new];
17 | });
18 | return device;
19 | }
20 |
21 | - (instancetype)init {
22 | if (self = [super init]) {
23 | NSBundle *bundle = NSBundle.mainBundle;
24 | _name = [bundle objectForInfoDictionaryKey:@"CFBundleDisplayName"];
25 | _version = [bundle objectForInfoDictionaryKey:@"CFBundleShortVersionString"];
26 | _build = [[bundle objectForInfoDictionaryKey:@"CFBundleVersion"] intValue];
27 | _docdir = [[NSFileManager.defaultManager URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
28 | }
29 | return self;
30 | }
31 |
32 |
33 | @end
34 |
--------------------------------------------------------------------------------
/Argus/Models/AGModel.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 | package com.wizjin.argus.model;
3 | option objc_class_prefix="AGM";
4 | option optimize_for = LITE_RUNTIME;
5 |
6 | enum Algorithm {
7 | ALGORITHM_UNSPECIFIED = 0;
8 | ALGORITHM_SHA1 = 1;
9 | ALGORITHM_SHA256 = 2;
10 | ALGORITHM_SHA512 = 3;
11 | ALGORITHM_MD5 = 4;
12 | }
13 |
14 | enum DigitCount {
15 | DIGIT_COUNT_UNSPECIFIED = 0;
16 | DIGIT_COUNT_SIX = 1;
17 | DIGIT_COUNT_EIGHT = 2;
18 | }
19 |
20 | enum OtpType {
21 | OTP_TYPE_UNSPECIFIED = 0;
22 | OTP_TYPE_HOTP = 1;
23 | OTP_TYPE_TOTP = 2;
24 | }
25 |
26 | message OtpParameters {
27 | bytes secret = 1;
28 | string name = 2;
29 | string issuer = 3;
30 | Algorithm algorithm = 4;
31 | DigitCount digits = 5;
32 | OtpType type = 6;
33 | int64 counter = 7;
34 | }
35 |
36 | message MigrationPayload {
37 | repeated OtpParameters parameters = 1;
38 | int32 version = 2;
39 | int32 batch_size = 3;
40 | int32 batch_index = 4;
41 | int32 batch_id = 5;
42 | }
43 |
--------------------------------------------------------------------------------
/Argus/Resources/Settings.bundle/Root.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | StringsTable
6 | Root
7 | PreferenceSpecifiers
8 |
9 |
10 | Type
11 | PSGroupSpecifier
12 | FooterText
13 | © Copyright 2021 wizjin.com. All rights reserved.
14 |
15 |
16 | Type
17 | PSTitleValueSpecifier
18 | Title
19 | Version
20 | Key
21 | version
22 | DefaultValue
23 |
24 |
25 |
26 | Type
27 | PSChildPaneSpecifier
28 | Title
29 | Acknowledgements
30 | File
31 | Acknowledgements
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/Podfile:
--------------------------------------------------------------------------------
1 | source 'https://github.com/CocoaPods/Specs.git'
2 |
3 | $project_name = 'Argus'
4 |
5 | platform :ios, '12.0'
6 |
7 | inhibit_all_warnings!
8 |
9 | target $project_name do
10 | pod 'JLRoutes'
11 | pod 'Masonry'
12 | pod 'Protobuf'
13 | pod 'XLForm'
14 |
15 | post_install do |installer|
16 | $version = installer.podfile.root_target_definitions[0].platform.deployment_target.to_s.to_f
17 | installer.pods_project.targets.each do |target|
18 | target.build_configurations.each do |config|
19 | config.build_settings['CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED'] = 'YES'
20 | if config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'].to_f < $version
21 | config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = $version.to_s
22 | end
23 | end
24 | end
25 | # Install acknowledgements
26 | $src = 'Pods/Target Support Files/Pods-%s/Pods-%s-acknowledgements.plist' % [$project_name, $project_name]
27 | $dst = '%s/Resources/Settings.bundle/Acknowledgements.plist' % $project_name
28 | FileUtils.cp_r($src, $dst, :remove_destination => true)
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/Argus/Views/Main/AGCreatedLabel.m:
--------------------------------------------------------------------------------
1 | //
2 | // AGCreatedLabel.m
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/12/8.
6 | //
7 |
8 | #import "AGCreatedLabel.h"
9 | #import "AGTheme.h"
10 |
11 | @implementation AGCreatedLabel
12 |
13 | - (instancetype)initWithFrame:(CGRect)frame {
14 | if (self = [super initWithFrame:frame]) {
15 | _created = 0;
16 | self.font = [UIFont systemFontOfSize:10];
17 | self.textColor = AGTheme.shared.minorLabelColor;
18 | }
19 | return self;
20 | }
21 |
22 | - (void)setCreated:(uint64_t)created {
23 | if (_created != created) {
24 | _created = created;
25 | if (created <= 0) {
26 | self.text = @"";
27 | } else {
28 | NSDate *date = [NSDate dateWithTimeIntervalSince1970:created/1000];
29 | NSDateFormatter *dateFormat = [NSDateFormatter new];
30 | [dateFormat setDateFormat:@"YYYY/MM/dd HH:mm"];
31 | self.text = [NSString stringWithFormat:@"Created at %@".localized, [dateFormat stringFromDate:date]];
32 | }
33 | }
34 | }
35 |
36 |
37 | @end
38 |
--------------------------------------------------------------------------------
/Argus/Models/AGMFAModel.h:
--------------------------------------------------------------------------------
1 | //
2 | // AGMFAModel.h
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/11/30.
6 | //
7 |
8 | #import
9 | #import
10 |
11 | NS_ASSUME_NONNULL_BEGIN
12 |
13 | @interface AGMFAModel : NSObject
14 |
15 | @property (nonatomic, readonly, strong) NSDictionary *data;
16 | @property (nonatomic, readonly, strong) NSString *title;
17 | @property (nonatomic, readonly, strong) NSString *detail;
18 | @property (nonatomic, readonly, strong) NSData *secret;
19 | @property (nonatomic, readonly, assign) NSInteger period;
20 | @property (nonatomic, readonly, assign) uint64_t digits;
21 | @property (nonatomic, readonly, assign) CCHmacAlgorithm algorithm;
22 | @property (nonatomic, readonly, assign) uint64_t created;
23 |
24 | + (instancetype)modelWithData:(NSDictionary *)data;
25 | - (BOOL)isEqual:(AGMFAModel *)other;
26 | - (uint64_t)calcT:(time_t)now remainder:(nullable uint64_t *)remainder;
27 | - (NSString *)calcCode:(uint64_t)t;
28 | - (NSString *)url;
29 | - (BOOL)canExportPB API_UNAVAILABLE(watchos);
30 |
31 |
32 | @end
33 |
34 | NS_ASSUME_NONNULL_END
35 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 wizjin
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Argus/Services/AGTheme.h:
--------------------------------------------------------------------------------
1 | //
2 | // AGTheme.h
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/11/30.
6 | //
7 |
8 | #import
9 |
10 | NS_ASSUME_NONNULL_BEGIN
11 |
12 | @interface AGTheme : NSObject
13 |
14 | @property (nonatomic, readonly, strong) UIColor *tintColor;
15 | @property (nonatomic, readonly, strong) UIColor *labelColor;
16 | @property (nonatomic, readonly, strong) UIColor *minorLabelColor;
17 | @property (nonatomic, readonly, strong) UIColor *infoColor;
18 | @property (nonatomic, readonly, strong) UIColor *warnColor;
19 | @property (nonatomic, readonly, strong) UIColor *alertColor;
20 | @property (nonatomic, readonly, strong) UIColor *secureColor;
21 | @property (nonatomic, readonly, strong) UIColor *backgroundColor;
22 | @property (nonatomic, readonly, strong) UIColor *cellBackgroundColor;
23 | @property (nonatomic, readonly, strong) UIColor *groupedBackgroundColor;
24 | @property (nonatomic, readonly, strong) UIImage *backImage;
25 | @property (nonatomic, readonly, strong) UIImage *clearImage;
26 | @property (nonatomic, assign) UIUserInterfaceStyle userInterfaceStyle API_AVAILABLE(ios(13.0));
27 |
28 | + (instancetype)shared;
29 |
30 |
31 | @end
32 |
33 | NS_ASSUME_NONNULL_END
34 |
--------------------------------------------------------------------------------
/Watch/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleDisplayName
8 | Argus
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 | UISupportedInterfaceOrientations
24 |
25 | UIInterfaceOrientationPortrait
26 | UIInterfaceOrientationPortraitUpsideDown
27 |
28 | WKCompanionAppBundleIdentifier
29 | com.wizjin.argus
30 | WKWatchKitApp
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/Watch Extension/Classes/CodeRowType.m:
--------------------------------------------------------------------------------
1 | //
2 | // CodeRowType.m
3 | // Watch Extension
4 | //
5 | // Created by WizJin on 2020/12/11.
6 | //
7 |
8 | #import "CodeRowType.h"
9 | #import "Theme.h"
10 |
11 | @interface CodeRowType ()
12 |
13 | @property (nonatomic, readonly, assign) uint64_t lastT;
14 |
15 | @end
16 |
17 | @implementation CodeRowType
18 |
19 | - (void)setModel:(AGMFAModel *)model {
20 | if (_model != model) {
21 | _model = model;
22 |
23 | _lastT = 0;
24 | [self.titleLabel setText:model.title];
25 | [self.accountLabel setText:model.detail];
26 | [self.timerLabel setText:@""];
27 | [self.codeLabel setText:@"--- ---"];
28 | [self.codeLabel setTextColor:Theme.tintColor];
29 | }
30 | }
31 |
32 | - (void)update:(time_t)now {
33 | uint64_t r = 0;
34 | uint64_t t = [self.model calcT:now remainder:&r];
35 | if (self.lastT != t) {
36 | _lastT = t;
37 | [self.codeLabel setText:[self.model calcCode:t].formatSpace];
38 | }
39 | if (self.model.period > 0 && r <= 5) {
40 | [self.codeLabel setTextColor:UIColor.redColor];
41 | } else {
42 | [self.codeLabel setTextColor:Theme.tintColor];
43 | }
44 | [self.timerLabel setText:[@(r) stringValue]];
45 | }
46 |
47 |
48 | @end
49 |
--------------------------------------------------------------------------------
/Argus/Services/AGMFAManager.h:
--------------------------------------------------------------------------------
1 | //
2 | // AGMFAManager.h
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/11/30.
6 | //
7 |
8 | #import
9 | #import "AGMFAModel.h"
10 | #import "AGMFAModel+GPB.h"
11 |
12 | NS_ASSUME_NONNULL_BEGIN
13 |
14 | @protocol AGMFAManagerDelegate
15 | @optional
16 | - (void)mfaUpdated;
17 | - (void)watchStatusChanged;
18 | @end
19 |
20 | @interface AGMFAManager : NSObject
21 |
22 | @property (nonatomic, readonly, assign) BOOL iCloudSyncEnabled;
23 |
24 | + (instancetype)shared;
25 | - (void)addDelegate:(id)delegate;
26 | - (void)removeDelegate:(id)delegate;
27 | - (BOOL)canOpenURL:(NSURL *)url;
28 | - (BOOL)openURL:(NSURL *)url;
29 | - (AGMFAModel *)itemAtIndex:(NSInteger)index;
30 | - (NSUInteger)itemCount;
31 | - (void)deleteItem:(AGMFAModel *)item completion:(void (^ __nullable)(void))completion;
32 | - (NSArray *)createExportURL:(NSArray *)models;
33 | - (void)copyToPasteboard:(nullable AGMFAModel *)item;
34 | - (void)active;
35 | - (void)deactive;
36 | - (BOOL)iCloudEnabled;
37 | - (void)setICloudSyncEnabled:(BOOL)iCloudSyncEnabled cleanup:(BOOL)cleanup;
38 | - (BOOL)hasWatch;
39 | - (BOOL)isWatchAppInstalled;
40 | - (BOOL)syncWatch:(BOOL)focus;
41 |
42 |
43 | @end
44 |
45 | NS_ASSUME_NONNULL_END
46 |
--------------------------------------------------------------------------------
/Watch Extension/Assets.xcassets/Complication.complicationset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "assets" : [
3 | {
4 | "filename" : "Circular.imageset",
5 | "idiom" : "watch",
6 | "role" : "circular"
7 | },
8 | {
9 | "filename" : "Extra Large.imageset",
10 | "idiom" : "watch",
11 | "role" : "extra-large"
12 | },
13 | {
14 | "filename" : "Graphic Bezel.imageset",
15 | "idiom" : "watch",
16 | "role" : "graphic-bezel"
17 | },
18 | {
19 | "filename" : "Graphic Circular.imageset",
20 | "idiom" : "watch",
21 | "role" : "graphic-circular"
22 | },
23 | {
24 | "filename" : "Graphic Corner.imageset",
25 | "idiom" : "watch",
26 | "role" : "graphic-corner"
27 | },
28 | {
29 | "filename" : "Graphic Extra Large.imageset",
30 | "idiom" : "watch",
31 | "role" : "graphic-extra-large"
32 | },
33 | {
34 | "filename" : "Graphic Large Rectangular.imageset",
35 | "idiom" : "watch",
36 | "role" : "graphic-large-rectangular"
37 | },
38 | {
39 | "filename" : "Modular.imageset",
40 | "idiom" : "watch",
41 | "role" : "modular"
42 | },
43 | {
44 | "filename" : "Utilitarian.imageset",
45 | "idiom" : "watch",
46 | "role" : "utilitarian"
47 | }
48 | ],
49 | "info" : {
50 | "author" : "xcode",
51 | "version" : 1
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/Watch Extension/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleDisplayName
8 | Argus Watch Extension
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 | CLKComplicationPrincipalClass
24 | ComplicationController
25 | NSExtension
26 |
27 | NSExtensionAttributes
28 |
29 | WKAppBundleIdentifier
30 | com.wizjin.argus.watchkitapp
31 |
32 | NSExtensionPointIdentifier
33 | com.apple.watchkit
34 |
35 | WKExtensionDelegateClassName
36 | ExtensionDelegate
37 |
38 |
39 |
--------------------------------------------------------------------------------
/Argus/Models/AGFile.m:
--------------------------------------------------------------------------------
1 | //
2 | // AGFile.m
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/12/16.
6 | //
7 |
8 | #import "AGFile.h"
9 |
10 | @interface AGFile ()
11 |
12 | @property (nonatomic, readonly, strong) NSDate *lastUpdate;
13 |
14 | @end
15 |
16 | @implementation AGFile
17 |
18 | - (instancetype)initWithURL:(NSURL *)url {
19 | if (self = [super init]) {
20 | _pathURL = url;
21 | _dataKey = nil;
22 | }
23 | return self;
24 | }
25 |
26 | - (BOOL)changed {
27 | return ![self.pathURL.modificationDate isEqualToDate:self.lastUpdate];
28 | }
29 |
30 | - (NSData *)fileData {
31 | NSError *error = nil;
32 | NSData *data = [NSData dataWithContentsOfURL:self.pathURL options:NSDataReadingUncached error:&error];
33 | if (error != nil) {
34 | data = [NSData new];
35 | _lastUpdate = nil;
36 | } else {
37 | _lastUpdate = self.pathURL.modificationDate;
38 | }
39 | _dataKey = data.sha1.hex;
40 | return data;
41 | }
42 |
43 | - (BOOL)write:(NSData *)data updateStatus:(BOOL)update {
44 | BOOL res = YES;
45 | if (data.length > 0) {
46 | res = [data writeToURL:self.pathURL atomically:YES];
47 | if (res) {
48 | if (update) {
49 | _lastUpdate = self.pathURL.modificationDate;
50 | }
51 | _dataKey = data.sha1.hex;
52 | }
53 | }
54 | return res;
55 | }
56 |
57 |
58 | @end
59 |
--------------------------------------------------------------------------------
/Argus/Extensions/UIImage+AGExt.m:
--------------------------------------------------------------------------------
1 | //
2 | // UIImage+AGExt.m
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/12/30.
6 | //
7 |
8 | #import "UIImage+AGExt.h"
9 |
10 | @implementation UIImage (AGExt)
11 |
12 | + (instancetype)imageWithSymbol:(NSString *)name {
13 | if (@available(iOS 13.0, *)) {
14 | return [UIImage systemImageNamed:name];
15 | } else {
16 | return [UIImage imageNamed:name];
17 | }
18 | }
19 |
20 | + (instancetype)imageWithSymbol:(NSString *)name height:(CGFloat)height {
21 | if (@available(iOS 13.0, *)) {
22 | return [UIImage systemImageNamed:name];
23 | } else {
24 | return [[UIImage imageNamed:name] resizeWithHeight:height];
25 | }
26 | }
27 |
28 | - (instancetype)resizeWithHeight:(CGFloat)height {
29 | UIImage *image = nil;
30 | if (self != nil) {
31 | CGSize size = self.size;
32 | size.width = size.width*height / size.height;
33 | size.height = height;
34 | UIGraphicsBeginImageContextWithOptions(size, NO, 0.0);
35 | [self drawInRect:CGRectMake(0, 0, size.width, size.height)];
36 | image = UIGraphicsGetImageFromCurrentImageContext();
37 | UIGraphicsEndImageContext();
38 | }
39 | return image;
40 | }
41 |
42 | - (instancetype)barItemImage {
43 | if (@available(iOS 13.0, *)) {
44 | return self;
45 | } else {
46 | return [self resizeWithHeight:22.0];
47 | }
48 | }
49 |
50 |
51 | @end
52 |
--------------------------------------------------------------------------------
/Argus/Views/Main/AGCountdownView.m:
--------------------------------------------------------------------------------
1 | //
2 | // AGCountdownView.m
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/11/30.
6 | //
7 |
8 | #import "AGCountdownView.h"
9 | #import "AGTheme.h"
10 |
11 | @interface AGCountdownView ()
12 |
13 | @property (nonatomic, readonly, assign) CGFloat rate;
14 |
15 | @end
16 |
17 | @implementation AGCountdownView
18 |
19 | - (instancetype)init {
20 | if (self = [super init]) {
21 | _rate = 0;
22 | _tintColor = AGTheme.shared.minorLabelColor;
23 | self.backgroundColor = UIColor.clearColor;
24 | }
25 | return self;
26 | }
27 |
28 | - (void)update:(AGMFAModel *)model remainder:(uint64_t)r {
29 | CGFloat rate = 0;
30 | CGFloat period = model.period;
31 | if (period > 0) {
32 | rate = (double)r/period;
33 | }
34 | if (self.rate != rate) {
35 | _rate = rate;
36 | [self setNeedsDisplay];
37 | }
38 | }
39 |
40 | - (void)drawRect:(CGRect)rect {
41 | CGRect rc = self.bounds;
42 | CGFloat redius = MIN(rc.size.width, rc.size.height) * 0.5;
43 | CGFloat rate = MIN(MAX(self.rate, 0), 1);
44 | CGContextRef ctx = UIGraphicsGetCurrentContext();
45 | CGContextSetFillColorWithColor(ctx, self.tintColor.CGColor);
46 | CGContextMoveToPoint(ctx, redius, redius);
47 | CGContextAddLineToPoint(ctx, redius, 0);
48 | CGContextAddArc(ctx, redius, redius, redius, -M_PI_2, -M_PI_2 - (M_PI * 2) * rate, 1);
49 | CGContextClosePath(ctx);
50 | CGContextFillPath(ctx);
51 | }
52 |
53 |
54 | @end
55 |
--------------------------------------------------------------------------------
/Argus/Extensions/UIButton+AGExt.m:
--------------------------------------------------------------------------------
1 | //
2 | // UIButton+AGExt.m
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/12/30.
6 | //
7 |
8 | #import "UIButton+AGExt.h"
9 |
10 | @interface AGImageButton : UIButton
11 |
12 | - (instancetype)initImage:(UIImage *)image target:(nullable id)target action:(nullable SEL)action;
13 |
14 | @end
15 |
16 | @implementation AGImageButton
17 |
18 | - (instancetype)initImage:(UIImage *)image target:(nullable id)target action:(nullable SEL)action {
19 | if (self = [super init]) {
20 | [self addTarget:target action:action forControlEvents:UIControlEventTouchUpInside];
21 | [self setImage:image forState:UIControlStateNormal];
22 | self.imageView.contentMode = UIViewContentModeScaleAspectFit;
23 | }
24 | return self;
25 | }
26 |
27 | - (CGRect)imageRectForContentRect:(CGRect)contentRect {
28 | CGRect rc = self.bounds;
29 | rc.origin.x = floor(contentRect.size.width * 0.22);
30 | rc.origin.y = floor(contentRect.size.height * 0.22);
31 | rc.size.width -= rc.origin.x * 2;
32 | rc.size.height -= rc.origin.y * 2;
33 | return rc;
34 | }
35 |
36 | @end
37 |
38 | @implementation UIButton (AGExt)
39 |
40 | + (instancetype)buttonWithImage:(UIImage *)image target:(nullable id)target action:(nullable SEL)action {
41 | if (@available(iOS 13.0, *)) {
42 | return [UIButton systemButtonWithImage:image target:target action:action];
43 | } else {
44 | return [[AGImageButton alloc] initImage:image target:target action:action];
45 | }
46 | }
47 |
48 |
49 | @end
50 |
--------------------------------------------------------------------------------
/Argus/Views/Utils/AGTableViewCell.m:
--------------------------------------------------------------------------------
1 | //
2 | // AGTableViewCell.m
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/12/18.
6 | //
7 |
8 | #import "AGTableViewCell.h"
9 | #import "AGTheme.h"
10 |
11 | @implementation AGTableViewCell
12 |
13 | - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(nullable NSString *)reuseIdentifier {
14 | if ([super initWithStyle:style reuseIdentifier:reuseIdentifier]) {
15 | self.backgroundColor = UIColor.clearColor;
16 | UIView *mainView = [UIView new];
17 | [self.contentView addSubview:(_mainView = mainView)];
18 | mainView.backgroundColor = AGTheme.shared.cellBackgroundColor;
19 | [mainView mas_makeConstraints:^(MASConstraintMaker *make) {
20 | make.top.left.right.equalTo(self.contentView);
21 | make.bottom.equalTo(self.contentView).offset(-kAGTableCellMargin);
22 | }];
23 | }
24 | return self;
25 | }
26 |
27 | - (void)setSelected:(BOOL)selected animated:(BOOL)animated {
28 | [self setHighlighted:selected animated:animated];
29 | }
30 |
31 | - (void)setHighlighted:(BOOL)highlighted animated:(BOOL)animated {
32 | CGFloat alpha = (highlighted ? 0.7 : 1);
33 | if (self.mainView.alpha != alpha) {
34 | if (!animated) {
35 | self.mainView.alpha = alpha;
36 | } else {
37 | [UIViewPropertyAnimator runningPropertyAnimatorWithDuration:0.2 delay:0 options:0 animations:^{
38 | self.mainView.alpha = alpha;
39 | } completion:nil];
40 | }
41 | }
42 | }
43 |
44 |
45 | @end
46 |
--------------------------------------------------------------------------------
/Argus/Classes/AppDelegate.m:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.m
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/11/29.
6 | //
7 |
8 | #import "AppDelegate.h"
9 | #import "AGRouter.h"
10 |
11 | @implementation AppDelegate
12 |
13 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
14 | return [AGRouter.shared launchWithOptions:launchOptions];
15 | }
16 |
17 | - (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary *)options {
18 | return [AGRouter.shared handleURL:url];
19 | }
20 |
21 | - (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(nonnull void (^)(NSArray> * _Nullable))restorationHandler {
22 | if ([userActivity.activityType isEqualToString:NSUserActivityTypeBrowsingWeb]) {
23 | [AGRouter.shared handleURL:userActivity.webpageURL];
24 | }
25 | return YES;
26 | }
27 |
28 | - (void)application:(UIApplication *)application performActionForShortcutItem:(UIApplicationShortcutItem *)shortcutItem completionHandler:(void(^)(BOOL succeeded))completionHandler {
29 | BOOL res = [AGRouter.shared handleShortcut:shortcutItem.type];
30 | if (completionHandler != NULL) {
31 | completionHandler(res);
32 | }
33 | }
34 |
35 | - (void)applicationDidBecomeActive:(UIApplication *)application {
36 | [AGRouter.shared active];
37 | }
38 |
39 | - (void)applicationDidEnterBackground:(UIApplication *)application {
40 | [AGRouter.shared deactive];
41 | }
42 |
43 |
44 | @end
45 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Argus - TOTP Authenticator Tools
2 |
3 | [](https://itunes.apple.com/app/id1542617858)
4 | [](LICENSE)
5 |
6 |
7 |
8 | Argus is an OTP Mobile App and support the Time-based One-time Password (TOTP) algorithm specified in RFC 6238.
9 |
10 | ## Getting Started
11 |
12 | 
13 |
14 | 1. Download Argus App from [iTunes App Store](https://itunes.apple.com/app/id1542617858).
15 | 2. Scan QrCode and import TOTP Record.
16 |
17 | ## Contributing
18 |
19 | Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are **greatly appreciated**.
20 |
21 | ## License
22 |
23 | Distributed under the MIT License. See [`LICENSE`](LICENSE) for more information.
24 |
25 | ## Acknowledgements
26 |
27 | * [RFC 6238](https://tools.ietf.org/rfc/rfc6238.txt)
28 | * [otpauth](https://github.com/google/google-authenticator/wiki/Key-Uri-Format)
29 | * [JLRoutes](https://github.com/joeldev/JLRoutes)
30 | * [Masonry](https://github.com/SnapKit/Masonry)
31 | * [Protobuf](https://github.com/protocolbuffers/protobuf)
32 | * [XLForm](https://github.com/xmartlabs/XLForm)
33 |
--------------------------------------------------------------------------------
/Argus/Resources/zh-Hans.lproj/Localizable.strings:
--------------------------------------------------------------------------------
1 | /*
2 | Localizable.strings (Chinese, Simplified)
3 | Argus
4 |
5 | Created by WizJin on 2020/11/29.
6 |
7 | */
8 | "OK"="确定";
9 | "Done"="完成";
10 | "Close"="关闭";
11 | "Cancel"="取消";
12 | "Delete"="删除";
13 | "Enable"="启用";
14 | "Disable"="禁用";
15 | "Settings"="设置";
16 | "Scan QR code"="扫描二维码";
17 | "Manual entry"="手工输入";
18 | "GENERAL"="通用";
19 | "Appearance"="外观";
20 | "Default"="系统默认";
21 | "Light"="浅色";
22 | "Dark"="深色";
23 | "Locker"="加锁保护";
24 | "DATA MANAGER"="数据管理";
25 | "iCloud has been disabled"="iCloud已被禁用";
26 | "Backup to iCloud"="将数据备份到iCloud";
27 | "Export with QR code"="使用二维码导出";
28 | "Export into text file"="导出到文本文件";
29 | "Export"="导出";
30 | "WATCH"="手表";
31 | "No watch app installed"="没有安装到Apple Watch";
32 | "Force data sync"="强制数据同步";
33 | "Sync data success!"="同步数据成功!";
34 | "Sync data failed!"="同步数据失败!";
35 | "ABOUT"="关于";
36 | "Version"="版本";
37 | "Acknowledgements"="版权声明";
38 | "Privacy Policy"="隐私协议";
39 | "No QR code"="没有发现二维码";
40 | "Code copied"="代码已复制";
41 | "Can't open url"="无法打开url";
42 | "Tap to reload"="轻触重载页面";
43 | "Release to reload"="松开重载页面";
44 | "Created at %@"="创建于%@";
45 | "NoMFA"="没有MFA\n点击添加记录";
46 | "NoMFATitle"="没有MFA记录";
47 | "NoMFAWatch"="可以从iPhone上的Argus应用添加记录";
48 | "TOTPInfoTitle"="扫描本二维码";
49 | "TOTPInfoDetail"="在您的新设备上下载Argus应用,并在应用内扫描本二维码";
50 | "Export success!"="导出记录成功!";
51 | "Export failed!"="导出记录失败!";
52 | "Export up to %@ records at a time."="一次最多导出%@条记录。";
53 | "This record format is not supported to export!"="不支持导出此记录格式!";
54 | "Add record success"="记录添加成功";
55 | "Record already exists"="记录已经存在";
56 | "Delete this record will NOT turn off OTP verification"="删除该条记录不会取消OTP验证";
57 | "Use password to unlock"="使用密码解锁";
58 |
--------------------------------------------------------------------------------
/Argus/Classes/Argus.pch:
--------------------------------------------------------------------------------
1 | //
2 | // Argus.pch
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/11/29.
6 | //
7 |
8 | #ifndef __ARGUS_PCH__
9 | #define __ARGUS_PCH__
10 |
11 | #define kAGPrivacyURL "https://argus.wizjin.com/privacy.html"
12 | #define kAGWatchLinkURL "itms-watchs://com.wizjin.argus.watchkitapp"
13 | #define kAGMFAFileName "mfa.dat"
14 | #define kAGiCloudContainer "iCloud.com.wizjin.argus"
15 |
16 | #if DEBUG
17 | # define ext_keywordify autoreleasepool {}
18 | #else
19 | # define ext_keywordify try {} @catch (...) {}
20 | #endif
21 |
22 | #define weakify(_x) ext_keywordify __weak __typeof__(_x) __weak_##_x##__ = _x;
23 | #define strongify(_x) ext_keywordify __strong __typeof__(_x) _x = __weak_##_x##__;
24 |
25 | #ifdef __OBJC__
26 |
27 | #if __has_include()
28 | # import "NSData+AGExt.h"
29 | #endif
30 |
31 | #if __has_include()
32 | # import "NSURL+AGExt.h"
33 | #endif
34 |
35 | #if __has_include()
36 | # import "NSString+AGExt.h"
37 | #endif
38 |
39 | #if __has_include()
40 | # import "UIColor+AGExt.h"
41 | #endif
42 |
43 | #if __has_include()
44 | # import "UIImage+AGExt.h"
45 | #endif
46 |
47 | #if __has_include()
48 | # import "UIButton+AGExt.h"
49 | #endif
50 |
51 | #if __has_include()
52 | # import
53 | # import "UIView+AGExt.h"
54 | #endif
55 |
56 | #if __has_include()
57 | # import "UIViewController+AGExt.h"
58 | #endif
59 |
60 | #if __has_include()
61 | # import "UIBarButtonItem+AGExt.h"
62 | #endif
63 |
64 | #endif /* __OBJC__ */
65 |
66 | #endif /* __ARGUS_PCH__ */
67 |
--------------------------------------------------------------------------------
/Argus/Extensions/NSString+AGExt.m:
--------------------------------------------------------------------------------
1 | //
2 | // NSString+AGExt.m
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/11/29.
6 | //
7 |
8 | #import "NSString+AGExt.h"
9 | #import
10 |
11 | @implementation NSString (AGExt)
12 |
13 | - (NSString *)localized {
14 | return [NSBundle.mainBundle localizedStringForKey:self value:@"" table:nil];
15 | }
16 |
17 | - (NSString *)formatSpace {
18 | NSMutableString *res = [[NSMutableString alloc] initWithCapacity:self.length + self.length/3];
19 | for (int i = 0; i < self.length; i++) {
20 | if (i%3 == 0 && i != 0) {
21 | [res appendString:@" "];
22 | }
23 | unichar c = [self characterAtIndex:i];
24 | [res appendFormat:@"%C", c];
25 | }
26 | return res;
27 | }
28 |
29 | - (NSString *)code {
30 | NSString *name = self;
31 | NSUInteger length = self.length;
32 | if (length > 0) {
33 | unichar *p = malloc(sizeof(unichar) * length);
34 | if (p != NULL) {
35 | int n = 0;
36 | BOOL upper = YES;
37 | const char *s = self.UTF8String;
38 | for (int i = 0; i < length; i++) {
39 | char c = s[i];
40 | if (c == '_' || c == '-') {
41 | upper = YES;
42 | continue;
43 | }
44 | if (upper) {
45 | c = toupper(c);
46 | upper = NO;
47 | }
48 | p[n++] = c;
49 | }
50 | name = [[NSString alloc] initWithCharactersNoCopy:p length:n freeWhenDone:YES];
51 | }
52 | }
53 | return name;
54 | }
55 |
56 | - (NSString *)trim {
57 | return [self stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceCharacterSet];
58 | }
59 |
60 |
61 | @end
62 |
--------------------------------------------------------------------------------
/Argus/Views/Editor/AGQRCodeView.m:
--------------------------------------------------------------------------------
1 | //
2 | // AGQRCodeView.m
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/12/8.
6 | //
7 |
8 | #import "AGQRCodeView.h"
9 |
10 | @implementation AGQRCodeView
11 |
12 | - (instancetype)initWithFrame:(CGRect)frame {
13 | if (self = [super initWithFrame:frame]) {
14 | self.backgroundColor = UIColor.whiteColor;
15 | self.layer.cornerRadius = 4.0;
16 | self.clipsToBounds = YES;
17 | }
18 | return self;
19 | }
20 |
21 | - (void)layoutSubviews {
22 | [super layoutSubviews];
23 | CGSize size = self.bounds.size;
24 | if (!CGSizeEqualToSize(size, self.image.size)) {
25 | CIFilter *filter = [CIFilter filterWithName:@"CIQRCodeGenerator"];
26 | [filter setValue:[self.url dataUsingEncoding:NSASCIIStringEncoding] forKey:@"inputMessage"];
27 | CIImage *image = filter.outputImage;
28 | if (image != nil) {
29 | CGRect extent = CGRectIntegral(image.extent);
30 | CGFloat scale = MIN(size.width/extent.size.width, size.height/extent.size.height);
31 | size_t with = scale * CGRectGetWidth(extent);
32 | size_t height = scale * CGRectGetHeight(extent);
33 | UIGraphicsBeginImageContext(CGSizeMake(with, height));
34 | CGContextRef imageContextRef = UIGraphicsGetCurrentContext();
35 | CIContext *context = [CIContext contextWithOptions:nil];
36 | CGImageRef outputImage = [context createCGImage:image fromRect:extent];
37 | CGContextSetInterpolationQuality(imageContextRef, kCGInterpolationNone);
38 | CGContextScaleCTM(imageContextRef, scale, scale);
39 | CGContextDrawImage(imageContextRef, extent, outputImage);
40 | self.image = UIGraphicsGetImageFromCurrentImageContext();
41 | CGImageRelease(outputImage);
42 | CGContextRelease(imageContextRef);
43 | }
44 | }
45 | }
46 |
47 | - (void)setUrl:(NSString *)url {
48 | if (![_url isEqualToString:url]) {
49 | _url = url;
50 | [self setNeedsLayout];
51 | }
52 | }
53 |
54 |
55 | @end
56 |
--------------------------------------------------------------------------------
/Argus/Resources/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/Argus/Resources/Base.lproj/Localizable.strings:
--------------------------------------------------------------------------------
1 | /*
2 | Localizable.strings (Base)
3 | Argus
4 |
5 | Created by WizJin on 2020/11/29.
6 |
7 | */
8 | "OK"="OK";
9 | "Done"="Done";
10 | "Close"="Close";
11 | "Cancel"="Cancel";
12 | "Delete"="Delete";
13 | "Enable"="Enable";
14 | "Disable"="Disable";
15 | "Settings"="Settings";
16 | "Scan QR code"="Scan QR code";
17 | "Manual entry"="Manual entry";
18 | "GENERAL"="GENERAL";
19 | "Appearance"="Appearance";
20 | "Default"="Default";
21 | "Light"="Light";
22 | "Dark"="Dark";
23 | "Locker"="Locker";
24 | "DATA MANAGER"="DATA MANAGER";
25 | "iCloud has been disabled"="iCloud has been disabled";
26 | "Backup to iCloud"="Backup to iCloud";
27 | "Export with QR code"="Export with QR code";
28 | "Export into text file"="Export into text file";
29 | "Export"="Export";
30 | "WATCH"="WATCH";
31 | "No watch app installed"="No watch app installed";
32 | "Force data sync"="Force data sync";
33 | "Sync data success!"="Sync data success!";
34 | "Sync data failed!"="Sync data failed!";
35 | "ABOUT"="ABOUT";
36 | "Version"="Version";
37 | "Acknowledgements"="Acknowledgements";
38 | "Privacy Policy"="Privacy Policy";
39 | "No QR code"="No QR code";
40 | "Code copied"="Code copied";
41 | "Can't open url"="Can't open url";
42 | "Tap to reload"="Tap to reload";
43 | "Release to reload"="Release to reload";
44 | "Created at %@"="Created at %@";
45 | "NoMFA"="No MFA Found\nTap and add record";
46 | "NoMFATitle"="No MFA Record";
47 | "NoMFAWatch"="You can add record in the Argus app on your iPhone.";
48 | "TOTPInfoTitle"="Scan this QR code";
49 | "TOTPInfoDetail"="Download the Argus app on your new device. Within the app, scan this QR code.";
50 | "Export success!"="Export success!";
51 | "Export failed!"="Export failed!";
52 | "Export up to %@ records at a time."="Export up to %@ records at a time.";
53 | "This record format is not supported to export!"="This record format is not supported to export!";
54 | "Add record success"="Add record success";
55 | "Record already exists"="Record already exists";
56 | "Delete this record will NOT turn off OTP verification"="Delete this record will NOT turn off OTP verification";
57 | "Use password to unlock"="Use password to unlock";
58 |
--------------------------------------------------------------------------------
/Watch/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "Icon-AppleWatch-24x24@2x.png",
5 | "idiom" : "watch",
6 | "role" : "notificationCenter",
7 | "scale" : "2x",
8 | "size" : "24x24",
9 | "subtype" : "38mm"
10 | },
11 | {
12 | "filename" : "Icon-AppleWatch-27.5x27.5@2x.png",
13 | "idiom" : "watch",
14 | "role" : "notificationCenter",
15 | "scale" : "2x",
16 | "size" : "27.5x27.5",
17 | "subtype" : "42mm"
18 | },
19 | {
20 | "filename" : "Icon-AppleWatch-29x29@2x.png",
21 | "idiom" : "watch",
22 | "role" : "companionSettings",
23 | "scale" : "2x",
24 | "size" : "29x29"
25 | },
26 | {
27 | "filename" : "Icon-AppleWatch-29x29@3x.png",
28 | "idiom" : "watch",
29 | "role" : "companionSettings",
30 | "scale" : "3x",
31 | "size" : "29x29"
32 | },
33 | {
34 | "filename" : "Icon-AppleWatch-40x40@2x.png",
35 | "idiom" : "watch",
36 | "role" : "appLauncher",
37 | "scale" : "2x",
38 | "size" : "40x40",
39 | "subtype" : "38mm"
40 | },
41 | {
42 | "filename" : "Icon-AppleWatch-44x44@2x.png",
43 | "idiom" : "watch",
44 | "role" : "appLauncher",
45 | "scale" : "2x",
46 | "size" : "44x44",
47 | "subtype" : "40mm"
48 | },
49 | {
50 | "filename" : "Icon-AppleWatch-50x50@2x.png",
51 | "idiom" : "watch",
52 | "role" : "appLauncher",
53 | "scale" : "2x",
54 | "size" : "50x50",
55 | "subtype" : "44mm"
56 | },
57 | {
58 | "filename" : "Icon-AppleWatch-86x86@2x.png",
59 | "idiom" : "watch",
60 | "role" : "quickLook",
61 | "scale" : "2x",
62 | "size" : "86x86",
63 | "subtype" : "38mm"
64 | },
65 | {
66 | "filename" : "Icon-AppleWatch-98x98@2x.png",
67 | "idiom" : "watch",
68 | "role" : "quickLook",
69 | "scale" : "2x",
70 | "size" : "98x98",
71 | "subtype" : "42mm"
72 | },
73 | {
74 | "filename" : "Icon-AppleWatch-108x108@2x.png",
75 | "idiom" : "watch",
76 | "role" : "quickLook",
77 | "scale" : "2x",
78 | "size" : "108x108",
79 | "subtype" : "44mm"
80 | },
81 | {
82 | "filename" : "Icon-AppleWatch-1024x1024.png",
83 | "idiom" : "watch-marketing",
84 | "scale" : "1x",
85 | "size" : "1024x1024"
86 | }
87 | ],
88 | "info" : {
89 | "author" : "xcode",
90 | "version" : 1
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # MacOSX
2 | #
3 | # General
4 | .DS_Store
5 | .AppleDouble
6 | .LSOverride
7 |
8 | # Icon must end with two \r
9 | Icon
10 |
11 | # Thumbnails
12 | ._*
13 |
14 | # Files that might appear in the root of a volume
15 | .DocumentRevisions-V100
16 | .fseventsd
17 | .Spotlight-V100
18 | .TemporaryItems
19 | .Trashes
20 | .VolumeIcon.icns
21 | .com.apple.timemachine.donotpresent
22 |
23 | # Directories potentially created on remote AFP share
24 | .AppleDB
25 | .AppleDesktop
26 | Network Trash Folder
27 | Temporary Items
28 | .apdisk
29 |
30 |
31 | # Xcode
32 | #
33 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
34 |
35 | ## User settings
36 | xcuserdata/
37 |
38 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
39 | *.xcscmblueprint
40 | *.xccheckout
41 |
42 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
43 | build/
44 | DerivedData/
45 | *.moved-aside
46 | *.pbxuser
47 | !default.pbxuser
48 | *.mode1v3
49 | !default.mode1v3
50 | *.mode2v3
51 | !default.mode2v3
52 | *.perspectivev3
53 | !default.perspectivev3
54 |
55 | ## Obj-C/Swift specific
56 | *.hmap
57 |
58 | ## App packaging
59 | *.ipa
60 | *.dSYM.zip
61 | *.dSYM
62 |
63 | # CocoaPods
64 | #
65 | # We recommend against adding the Pods directory to your .gitignore. However
66 | # you should judge for yourself, the pros and cons are mentioned at:
67 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
68 | #
69 | Pods/
70 |
71 | # Add this line if you want to avoid checking in source code from the Xcode workspace
72 | # *.xcworkspace
73 |
74 | # Carthage
75 | #
76 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
77 | Carthage/Checkouts
78 | Carthage/Build/
79 |
80 | # fastlane
81 | #
82 | # It is recommended to not store the screenshots in the git repo.
83 | # Instead, use fastlane to re-generate the screenshots whenever they are needed.
84 | # For more information about the recommended setup visit:
85 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
86 |
87 | fastlane/report.xml
88 | fastlane/Preview.html
89 | fastlane/screenshots/**/*.png
90 | fastlane/test_output
91 |
92 | # Code Injection
93 | #
94 | # After new code Injection tools there's a generated folder /iOSInjectionProject
95 | # https://github.com/johnno1962/injectionforxcode
96 |
97 | iOSInjectionProject/
98 |
99 | # Project
100 | Acknowledgements.plist
101 |
102 |
--------------------------------------------------------------------------------
/Argus/Views/Main/AGMFAEmptyView.m:
--------------------------------------------------------------------------------
1 | //
2 | // AGMFAEmptyView.m
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/12/1.
6 | //
7 |
8 | #import "AGMFAEmptyView.h"
9 | #import "AGTheme.h"
10 |
11 | @interface AGMFAEmptyView ()
12 |
13 | @property (nonatomic, readonly, strong) UITapGestureRecognizer *recognizer;
14 |
15 | @end
16 |
17 | @implementation AGMFAEmptyView
18 |
19 | - (instancetype)initWithTarget:(id)target action:(SEL)action {
20 | if (self = [super init]) {
21 | AGTheme *theme = AGTheme.shared;
22 | self.backgroundColor = theme.groupedBackgroundColor;
23 | self.alwaysBounceVertical = YES;
24 | self.alwaysBounceHorizontal = NO;
25 | self.showsVerticalScrollIndicator = NO;
26 | self.showsHorizontalScrollIndicator = NO;
27 | if (@available(iOS 13.0, *)) {
28 | self.automaticallyAdjustsScrollIndicatorInsets = NO;
29 | }
30 |
31 | UIImageView *imageView = [[UIImageView alloc] initWithImage:[UIImage imageWithSymbol:@"rectangle.stack.badge.plus"]];
32 | [self addSubview:imageView];
33 | imageView.tintColor = theme.minorLabelColor;
34 | [imageView mas_makeConstraints:^(MASConstraintMaker *make) {
35 | make.size.mas_equalTo(CGSizeMake(80, 80));
36 | make.centerX.equalTo(self);
37 | make.top.equalTo(self).offset(UIScreen.mainScreen.bounds.size.height * 0.25);
38 | }];
39 |
40 | UILabel *titleLabel = [UILabel new];
41 | [self addSubview:titleLabel];
42 | titleLabel.numberOfLines = 0;
43 | NSMutableParagraphStyle *style = [NSMutableParagraphStyle new];
44 | style.alignment = NSTextAlignmentCenter;
45 | style.lineSpacing = 16;
46 | NSMutableAttributedString *text = [[NSMutableAttributedString alloc] initWithString:@"NoMFA".localized];
47 | [text addAttributes:@{
48 | NSFontAttributeName: [UIFont systemFontOfSize:18],
49 | NSForegroundColorAttributeName: theme.minorLabelColor,
50 | NSParagraphStyleAttributeName:style,
51 | } range:NSMakeRange(0, text.length)];
52 | titleLabel.attributedText = text;
53 | [titleLabel mas_makeConstraints:^(MASConstraintMaker *make) {
54 | make.centerX.equalTo(imageView);
55 | make.top.equalTo(imageView.mas_bottom).offset(40);
56 | }];
57 |
58 | _recognizer = [[UITapGestureRecognizer alloc] initWithTarget:target action:action];
59 | [self addGestureRecognizer:self.recognizer];
60 | }
61 | return self;
62 | }
63 |
64 | - (void)dealloc {
65 | [self removeGestureRecognizer:self.recognizer];
66 | }
67 |
68 |
69 | @end
70 |
--------------------------------------------------------------------------------
/Argus/Views/Main/AGCodeView.m:
--------------------------------------------------------------------------------
1 | //
2 | // AGCodeView.m
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/12/8.
6 | //
7 |
8 | #import "AGCodeView.h"
9 | #import "AGTheme.h"
10 |
11 | @interface AGCodeView ()
12 |
13 | @property (nonatomic, readonly, assign) uint64_t lastT;
14 | @property (nonatomic, nullable, strong) UIViewPropertyAnimator *animator;
15 |
16 | @end
17 |
18 | @implementation AGCodeView
19 |
20 | - (instancetype)initWithFrame:(CGRect)frame {
21 | if (self = [super initWithFrame:frame]) {
22 | _lastT = 0;
23 | _fontSize = 50;
24 | _animator = nil;
25 | self.layer.backgroundColor = UIColor.clearColor.CGColor;
26 | self.font = [UIFont fontWithName:@"Helvetica Neue" size:self.fontSize];
27 | self.textColor = AGTheme.shared.tintColor;
28 | }
29 | return self;
30 | }
31 |
32 | - (void)setFontSize:(CGFloat)fontSize {
33 | if (_fontSize != fontSize) {
34 | _fontSize = fontSize;
35 | self.font = [UIFont fontWithName:@"Helvetica Neue" size:self.fontSize];
36 | }
37 | }
38 |
39 | - (void)reset {
40 | _lastT = 0;
41 | [self stopFlashAnimator];
42 | }
43 |
44 | - (uint64_t)update:(AGMFAModel *)model now:(time_t)now {
45 | uint64_t r = 0;
46 | uint64_t t = [model calcT:now remainder:&r];
47 | if (self.lastT != t) {
48 | _lastT = t;
49 | self.text = [model calcCode:t].formatSpace;
50 | }
51 | AGTheme *theme = AGTheme.shared;
52 | if (model.period > 0 && r <= 5) {
53 | self.textColor = theme.alertColor;
54 | [self startFlashAnimator];
55 | } else {
56 | self.textColor = theme.tintColor;
57 | [self stopFlashAnimator];
58 | }
59 | return r;
60 | }
61 |
62 | - (void)startFlashAnimator {
63 | if (self.animator == nil) {
64 | self.alpha = 1.0;
65 | self.animator = [UIViewPropertyAnimator runningPropertyAnimatorWithDuration:0.5 delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
66 | #pragma clang diagnostic push
67 | #pragma clang diagnostic ignored "-Wdeprecated-declarations"
68 | [UIView setAnimationRepeatCount:MAXFLOAT];
69 | [UIView setAnimationRepeatAutoreverses:YES];
70 | #pragma clang diagnostic pop
71 | self.alpha = 0.4;
72 | } completion:^(UIViewAnimatingPosition finalPosition) {
73 | [self stopFlashAnimator];
74 | }];
75 | }
76 | }
77 |
78 | - (void)stopFlashAnimator {
79 | if (self.animator != nil) {
80 | if (self.animator.isRunning) {
81 | [self.animator stopAnimation:YES];
82 | }
83 | self.animator = nil;
84 | self.alpha = 1.0;
85 | }
86 | }
87 |
88 |
89 | @end
90 |
--------------------------------------------------------------------------------
/Argus/Services/AGTheme.m:
--------------------------------------------------------------------------------
1 | //
2 | // AGTheme.m
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/11/30.
6 | //
7 |
8 | #import "AGTheme.h"
9 | #import "AGRouter.h"
10 |
11 | @implementation AGTheme
12 |
13 | + (instancetype)shared {
14 | static AGTheme *theme;
15 | static dispatch_once_t onceToken;
16 | dispatch_once(&onceToken, ^{
17 | theme = [AGTheme new];
18 | });
19 | return theme;
20 | }
21 |
22 | - (instancetype)init {
23 | if (self = [super init]) {
24 | if (@available(iOS 13.0, *)) {
25 | _labelColor = UIColor.labelColor;
26 | _minorLabelColor = UIColor.secondaryLabelColor;
27 | _backgroundColor = UIColor.systemBackgroundColor;
28 | _groupedBackgroundColor = UIColor.systemGroupedBackgroundColor;
29 | _backImage = [UIImage imageWithSymbol:@"chevron.backward"];
30 |
31 | self.userInterfaceStyle = [NSUserDefaults.standardUserDefaults integerForKey:@"userInterfaceStyle"];
32 | } else {
33 | _labelColor = UIColor.blackColor;
34 | _minorLabelColor = [UIColor colorWithRGBA:0x3c3c4399];
35 | _backgroundColor = UIColor.whiteColor;
36 | _groupedBackgroundColor = [UIColor colorWithRGBA:0xf2f2f7ff];
37 | _backImage = [UIImage imageWithSymbol:@"chevron.backward"].barItemImage;
38 | }
39 |
40 | _tintColor = [UIColor colorNamed:@"AccentColor"];
41 | _infoColor = UIColor.systemGreenColor;
42 | _warnColor = UIColor.systemYellowColor;
43 | _alertColor = UIColor.systemRedColor;
44 | _secureColor = UIColor.systemGreenColor;
45 | _cellBackgroundColor = [UIColor colorNamed:@"CellColor"];
46 | _clearImage = [UIImage new];
47 |
48 | // Appearance
49 | UINavigationBar *navigationBar = UINavigationBar.appearance;
50 | navigationBar.shadowImage = self.clearImage;
51 | navigationBar.tintColor = self.labelColor;
52 | navigationBar.barTintColor = self.backgroundColor;
53 | navigationBar.backgroundColor = self.backgroundColor;
54 | navigationBar.backIndicatorImage = self.backImage;
55 | navigationBar.backIndicatorTransitionMaskImage = self.backImage;
56 |
57 | UISwitch.appearance.onTintColor = self.tintColor;
58 | UIProgressView.appearance.tintColor = self.tintColor;
59 | }
60 | return self;
61 | }
62 |
63 | - (UIUserInterfaceStyle)userInterfaceStyle API_AVAILABLE(ios(13.0)) {
64 | return AGRouter.shared.window.overrideUserInterfaceStyle;
65 | }
66 |
67 | - (void)setUserInterfaceStyle:(UIUserInterfaceStyle)userInterfaceStyle API_AVAILABLE(ios(13.0)) {
68 | if (userInterfaceStyle != self.userInterfaceStyle) {
69 | AGRouter.shared.window.overrideUserInterfaceStyle = userInterfaceStyle;
70 | [NSUserDefaults.standardUserDefaults setInteger:userInterfaceStyle forKey:@"userInterfaceStyle"];
71 | }
72 | }
73 |
74 |
75 | @end
76 |
--------------------------------------------------------------------------------
/Argus/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "AppIcon-20x20@2x.png",
5 | "idiom" : "iphone",
6 | "scale" : "2x",
7 | "size" : "20x20"
8 | },
9 | {
10 | "filename" : "AppIcon-20x20@3x.png",
11 | "idiom" : "iphone",
12 | "scale" : "3x",
13 | "size" : "20x20"
14 | },
15 | {
16 | "filename" : "AppIcon-29x29@2x.png",
17 | "idiom" : "iphone",
18 | "scale" : "2x",
19 | "size" : "29x29"
20 | },
21 | {
22 | "filename" : "AppIcon-29x29@3x.png",
23 | "idiom" : "iphone",
24 | "scale" : "3x",
25 | "size" : "29x29"
26 | },
27 | {
28 | "filename" : "AppIcon-40x40@2x.png",
29 | "idiom" : "iphone",
30 | "scale" : "2x",
31 | "size" : "40x40"
32 | },
33 | {
34 | "filename" : "AppIcon-40x40@3x.png",
35 | "idiom" : "iphone",
36 | "scale" : "3x",
37 | "size" : "40x40"
38 | },
39 | {
40 | "filename" : "AppIcon-60x60@2x.png",
41 | "idiom" : "iphone",
42 | "scale" : "2x",
43 | "size" : "60x60"
44 | },
45 | {
46 | "filename" : "AppIcon-60x60@3x.png",
47 | "idiom" : "iphone",
48 | "scale" : "3x",
49 | "size" : "60x60"
50 | },
51 | {
52 | "filename" : "AppIcon-20x20@1x.png",
53 | "idiom" : "ipad",
54 | "scale" : "1x",
55 | "size" : "20x20"
56 | },
57 | {
58 | "filename" : "AppIcon-20x20@2x.png",
59 | "idiom" : "ipad",
60 | "scale" : "2x",
61 | "size" : "20x20"
62 | },
63 | {
64 | "filename" : "AppIcon-29x29@1x.png",
65 | "idiom" : "ipad",
66 | "scale" : "1x",
67 | "size" : "29x29"
68 | },
69 | {
70 | "filename" : "AppIcon-29x29@2x.png",
71 | "idiom" : "ipad",
72 | "scale" : "2x",
73 | "size" : "29x29"
74 | },
75 | {
76 | "filename" : "AppIcon-40x40@1x.png",
77 | "idiom" : "ipad",
78 | "scale" : "1x",
79 | "size" : "40x40"
80 | },
81 | {
82 | "filename" : "AppIcon-40x40@2x.png",
83 | "idiom" : "ipad",
84 | "scale" : "2x",
85 | "size" : "40x40"
86 | },
87 | {
88 | "filename" : "AppIcon-76x76@1x.png",
89 | "idiom" : "ipad",
90 | "scale" : "1x",
91 | "size" : "76x76"
92 | },
93 | {
94 | "filename" : "AppIcon-76x76@2x.png",
95 | "idiom" : "ipad",
96 | "scale" : "2x",
97 | "size" : "76x76"
98 | },
99 | {
100 | "filename" : "AppIcon-83.5x83.5@2x.png",
101 | "idiom" : "ipad",
102 | "scale" : "2x",
103 | "size" : "83.5x83.5"
104 | },
105 | {
106 | "filename" : "AppIcon-iTunes.png",
107 | "idiom" : "ios-marketing",
108 | "scale" : "1x",
109 | "size" : "1024x1024"
110 | }
111 | ],
112 | "info" : {
113 | "author" : "xcode",
114 | "version" : 1
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/Argus/Models/AGMFAStorage.m:
--------------------------------------------------------------------------------
1 | //
2 | // AGMFAStorage.m
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/12/12.
6 | //
7 |
8 | #import "AGMFAStorage.h"
9 |
10 | @interface AGMFAStorage ()
11 |
12 | @property (nonatomic, readonly, strong) NSMutableArray *items;
13 |
14 | @end
15 |
16 | @implementation AGMFAStorage
17 |
18 | - (instancetype)initWithURL:(NSURL *)url {
19 | if (self = [super initWithURL:url]) {
20 | _items = [NSMutableArray new];
21 | }
22 | return self;
23 | }
24 |
25 | - (BOOL)containsItem:(AGMFAModel *)item {
26 | return [self.items containsObject:item];
27 | }
28 |
29 | - (void)removeItem:(AGMFAModel *)item {
30 | [self.items removeObject:item];
31 | }
32 |
33 | - (void)addItem:(AGMFAModel *)item {
34 | [self.items addObject:item];
35 | }
36 |
37 | - (AGMFAModel *)itemAtIndex:(NSInteger)index {
38 | return [self.items objectAtIndex:index];
39 | }
40 |
41 | - (NSUInteger)count {
42 | return self.items.count;
43 | }
44 |
45 | - (BOOL)load {
46 | BOOL res = NO;
47 | if (!self.changed) {
48 | res = YES;
49 | } else {
50 | NSError *error = nil;
51 | NSData *fileData = self.fileData;
52 | NSData *data = [fileData decompress];
53 | if (data .length > 0) {
54 | NSDictionary *item = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingFragmentsAllowed error:&error];
55 | if (error == nil && item != nil) {
56 | NSArray *items = [item valueForKey:@"items"];
57 | NSMutableArray *mfaItems = [NSMutableArray arrayWithCapacity:items.count];
58 | for (NSDictionary *item in items) {
59 | AGMFAModel *mfa = [AGMFAModel modelWithData:item];
60 | if (mfa != nil) {
61 | [mfaItems addObject:mfa];
62 | }
63 | }
64 | _items = mfaItems;
65 | res = YES;
66 | }
67 | }
68 | }
69 | return res;
70 | }
71 |
72 | - (BOOL)save {
73 | BOOL res = NO;
74 | NSMutableArray *items = [NSMutableArray arrayWithCapacity:self.items.count];
75 | for (AGMFAModel *model in self.items) {
76 | [items addObject:model.data];
77 | }
78 | NSError *error = nil;
79 | NSData *data = [[NSJSONSerialization dataWithJSONObject:@{ @"items": items } options:NSJSONWritingSortedKeys error:&error] compress];
80 | if (error == nil && data.length > 0) {
81 | [self write:data updateStatus:YES];
82 | res = YES;
83 | }
84 | return res;
85 | }
86 |
87 | - (BOOL)saveData:(NSData *)data {
88 | BOOL res = NO;
89 | if (data.length > 0 && self.dataKey != nil) {
90 | NSString *key = data.sha1.hex;
91 | if ([self.dataKey isEqualToString:key]) {
92 | res = YES;
93 | } else {
94 | if ([self write:data updateStatus:NO]) {
95 | res = YES;
96 | }
97 | }
98 | }
99 | return res;
100 | }
101 |
102 |
103 | @end
104 |
--------------------------------------------------------------------------------
/Argus/Resources/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleDisplayName
8 | $(PRODUCT_NAME)
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 | CFBundleURLTypes
22 |
23 |
24 | CFBundleTypeRole
25 | Editor
26 | CFBundleURLName
27 | otpauth
28 | CFBundleURLSchemes
29 |
30 | otpauth
31 | otpauth-migration
32 |
33 |
34 |
35 | CFBundleVersion
36 | $(CURRENT_PROJECT_VERSION)
37 | ITSAppUsesNonExemptEncryption
38 |
39 | LSRequiresIPhoneOS
40 |
41 | NSCameraUsageDescription
42 | To scan a QR code when you need it.
43 | NSFaceIDUsageDescription
44 | Use FaceID to unlock app when you need it.
45 | NSPhotoLibraryAddUsageDescription
46 | To save export QR code into photo library when you need it.
47 | NSPhotoLibraryUsageDescription
48 | To picker a QR code from photo when you need it.
49 | UIApplicationSceneManifest
50 |
51 | UIApplicationSupportsMultipleScenes
52 |
53 |
54 | UIApplicationShortcutItems
55 |
56 |
57 | UIApplicationShortcutItemIconFile
58 | qrcode.viewfinder
59 | UIApplicationShortcutItemTitle
60 | ScanActionTitle
61 | UIApplicationShortcutItemType
62 | ScanAction
63 |
64 |
65 | UIApplicationSupportsIndirectInputEvents
66 |
67 | UILaunchStoryboardName
68 | LaunchScreen
69 | UIRequiredDeviceCapabilities
70 |
71 | armv7
72 |
73 | UIStatusBarHidden
74 |
75 | UIStatusBarStyle
76 | UIStatusBarStyleDefault
77 | UISupportedInterfaceOrientations
78 |
79 | UIInterfaceOrientationPortrait
80 |
81 | UISupportedInterfaceOrientations~ipad
82 |
83 | UIInterfaceOrientationPortrait
84 | UIInterfaceOrientationPortraitUpsideDown
85 | UIInterfaceOrientationLandscapeLeft
86 | UIInterfaceOrientationLandscapeRight
87 |
88 |
89 |
90 |
--------------------------------------------------------------------------------
/Argus/Views/Main/AGMFATableView.m:
--------------------------------------------------------------------------------
1 | //
2 | // AGMFATableView.m
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/12/1.
6 | //
7 |
8 | #import "AGMFATableView.h"
9 | #import "AGMFATableViewCell.h"
10 | #import "AGTheme.h"
11 |
12 | @interface AGMFATableView ()
13 |
14 | @property (nonatomic, nullable, strong) UIView *emptyView;
15 |
16 | @end
17 |
18 | @implementation AGMFATableView
19 |
20 | - (instancetype)init {
21 | if (self = [super initWithFrame:CGRectZero style:UITableViewStylePlain]) {
22 | self.backgroundColor = AGTheme.shared.groupedBackgroundColor;
23 | self.tableHeaderView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 0, kAGTableCellMargin)];
24 | self.separatorStyle = UITableViewCellSeparatorStyleNone;
25 | self.showsHorizontalScrollIndicator = NO;
26 | self.rowHeight = kAGMFACellHeight + kAGTableCellMargin;
27 | }
28 | return self;
29 | }
30 |
31 | - (void)layoutSubviews {
32 | [super layoutSubviews];
33 | for (UIView *subview in self.subviews) {
34 | if ([NSStringFromClass(subview.class) isEqualToString:@"_UITableViewCellSwipeContainerView"]) {
35 | UIView *button = [subview findWithClassName:@"UISwipeActionPullView"];
36 | if (button != nil) {
37 | NSIndexPath *indexPath = [button valueForKeyPath:@"_delegate._indexPath"];
38 | if (indexPath != nil) {
39 | AGMFATableViewCell *cell = [self cellForRowAtIndexPath:indexPath];
40 | if (cell != nil) {
41 | CGRect frame = button.frame;
42 | frame.size.height = kAGMFACellHeight;
43 | button.frame = frame;
44 | }
45 | }
46 | }
47 | }
48 | }
49 | }
50 |
51 | - (void)deleteRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation {
52 | [super deleteRowsAtIndexPaths:indexPaths withRowAnimation:animation];
53 | [self updateDataUI];
54 | }
55 |
56 | - (void)reloadData {
57 | [super reloadData];
58 | [self updateDataUI];
59 | }
60 |
61 | #pragma mark - Private Methods
62 | - (void)updateDataUI {
63 | if ([self numberOfRowsInSection:0] > 0) {
64 | if (_emptyView != nil) {
65 | [_emptyView removeFromSuperview];
66 | _emptyView = nil;
67 | }
68 | } else {
69 | if (_emptyView == nil) {
70 | #pragma clang diagnostic push
71 | #pragma clang diagnostic ignored "-Wundeclared-selector"
72 | if ([self.delegate respondsToSelector:@selector(tableViewEmptyView:)]) {
73 | UIView *emptyView = [self.delegate performSelector:@selector(tableViewEmptyView:) withObject:self];
74 | if (emptyView != nil) {
75 | [emptyView removeFromSuperview];
76 | [self.superview addSubview:(_emptyView = emptyView)];
77 | [emptyView mas_makeConstraints:^(MASConstraintMaker *make) {
78 | make.edges.equalTo(self);
79 | }];
80 | }
81 | }
82 | #pragma clang diagnostic pop
83 | }
84 | }
85 | }
86 |
87 |
88 | @end
89 |
--------------------------------------------------------------------------------
/Argus/Views/Settings/AGAcknowledgementsViewController.m:
--------------------------------------------------------------------------------
1 | //
2 | // AGAcknowledgementsViewController.m
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/11/30.
6 | //
7 |
8 | #import "AGAcknowledgementsViewController.h"
9 | #import "AGTheme.h"
10 |
11 | @implementation AGAcknowledgementsViewController
12 |
13 | - (void)viewDidLoad {
14 | [super viewDidLoad];
15 |
16 | self.title = @"Acknowledgements".localized;
17 |
18 | AGTheme *theme = AGTheme.shared;
19 |
20 | UIScrollView *view = [UIScrollView new];
21 | [self.view addSubview:view];
22 | view.alwaysBounceVertical = YES;
23 | [view mas_makeConstraints:^(MASConstraintMaker *make) {
24 | make.edges.equalTo(self.view);
25 | }];
26 |
27 | UIView *lastView = nil;
28 | UIFont *titleFont = [UIFont boldSystemFontOfSize:18];
29 | UIFont *contextFont = [UIFont systemFontOfSize:14];
30 |
31 | NSURL *url = [[NSBundle.mainBundle URLForResource:@"Settings" withExtension:@"bundle"] URLByAppendingPathComponent:@"Acknowledgements.plist"];
32 | NSDictionary *data = [self loadPlist:url];
33 | for (NSDictionary *item in [data valueForKey:@"PreferenceSpecifiers"]) {
34 | NSString *title = [item valueForKey:@"Title"];
35 | if (title.length > 0) {
36 | if (lastView == nil) {
37 | lastView = [UIView new];
38 | [view addSubview:lastView];
39 | [lastView mas_makeConstraints:^(MASConstraintMaker *make) {
40 | make.left.equalTo(self.view).offset(20);
41 | make.right.equalTo(self.view).offset(-20);
42 | make.top.equalTo(view).offset(8);
43 | }];
44 | } else {
45 | UILabel *label = [UILabel new];
46 | [view addSubview:label];
47 | label.font = titleFont;
48 | label.text = title;
49 | label.textColor = theme.labelColor;
50 | [label mas_makeConstraints:^(MASConstraintMaker *make) {
51 | make.left.right.equalTo(lastView);
52 | make.top.equalTo(lastView.mas_bottom).offset(20);
53 | }];
54 | lastView = label;
55 | }
56 | }
57 | NSString *context = [item valueForKey:@"FooterText"];
58 | if (context.length > 0) {
59 | UILabel *label = [UILabel new];
60 | [view addSubview:label];
61 | label.numberOfLines = 0;
62 | label.font = contextFont;
63 | label.textColor = theme.minorLabelColor;
64 | label.text = [context stringByAppendingString:@"\n"];
65 | [label mas_makeConstraints:^(MASConstraintMaker *make) {
66 | make.left.right.equalTo(lastView);
67 | make.top.equalTo(lastView.mas_bottom).offset(20);
68 | }];
69 | lastView = label;
70 | }
71 | }
72 | [lastView mas_makeConstraints:^(MASConstraintMaker *make) {
73 | make.bottom.equalTo(view);
74 | }];
75 | }
76 |
77 | #pragma mark - Private Methods
78 | - (NSDictionary *)loadPlist:(NSURL *)url {
79 | NSError *error = nil;
80 | NSDictionary *result = nil;
81 | NSData *data = [NSData dataWithContentsOfURL:url options:NSDataReadingMappedIfSafe error:&error];
82 | if (error == nil && data.length > 0) {
83 | NSDictionary *plist = [NSPropertyListSerialization propertyListWithData:data options:0 format:nil error:&error];
84 | if (error == nil) {
85 | result = plist;
86 | }
87 | }
88 | return result;
89 | }
90 |
91 |
92 | @end
93 |
--------------------------------------------------------------------------------
/Argus/Views/Export/AGExportTableViewCell.m:
--------------------------------------------------------------------------------
1 | //
2 | // AGExportTableViewCell.m
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/12/18.
6 | //
7 |
8 | #import "AGExportTableViewCell.h"
9 | #import "AGCreatedLabel.h"
10 | #import "AGTheme.h"
11 |
12 | @interface AGExportTableViewCell ()
13 |
14 | @property (nonatomic, readonly, strong) UIImageView *checkImage;
15 | @property (nonatomic, readonly, strong) UILabel *titleLabel;
16 | @property (nonatomic, readonly, strong) UILabel *detailLabel;
17 | @property (nonatomic, readonly, strong) AGCreatedLabel *createdLabel;
18 |
19 | @end
20 |
21 | @implementation AGExportTableViewCell
22 |
23 | - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(nullable NSString *)reuseIdentifier {
24 | if ([super initWithStyle:style reuseIdentifier:reuseIdentifier]) {
25 | AGTheme *theme = AGTheme.shared;
26 |
27 | UIImageView *checkImage = [UIImageView new];
28 | [self.mainView addSubview:(_checkImage = checkImage)];
29 | [checkImage mas_makeConstraints:^(MASConstraintMaker *make) {
30 | make.centerY.equalTo(self.mainView);
31 | make.left.equalTo(self.mainView).offset(20);
32 | make.size.mas_equalTo(CGSizeMake(32, 32));
33 | }];
34 |
35 | UILabel *titleLabel = [UILabel new];
36 | [self.mainView addSubview:(_titleLabel = titleLabel)];
37 | [titleLabel mas_makeConstraints:^(MASConstraintMaker *make) {
38 | make.top.equalTo(self.mainView).offset(16);
39 | make.left.equalTo(checkImage.mas_right).offset(16);
40 | }];
41 | titleLabel.font = [UIFont boldSystemFontOfSize:20];
42 | titleLabel.textColor = theme.labelColor;
43 |
44 | UILabel *detailLabel = [UILabel new];
45 | [self.mainView addSubview:(_detailLabel = detailLabel)];
46 | [detailLabel mas_makeConstraints:^(MASConstraintMaker *make) {
47 | make.bottom.equalTo(self.mainView).offset(-16);
48 | make.left.equalTo(titleLabel);
49 | }];
50 | detailLabel.font = [UIFont systemFontOfSize:14];
51 | detailLabel.textColor = theme.labelColor;
52 |
53 | AGCreatedLabel *createdLabel = [AGCreatedLabel new];
54 | [self.mainView addSubview:(_createdLabel = createdLabel)];
55 | [createdLabel mas_makeConstraints:^(MASConstraintMaker *make) {
56 | make.centerY.equalTo(titleLabel);
57 | make.right.equalTo(self.mainView).offset(-16);
58 | }];
59 |
60 | _model = nil;
61 | }
62 | return self;
63 | }
64 |
65 | - (void)setSelected:(BOOL)selected animated:(BOOL)animated {
66 | if (self.model.canExportPB) {
67 | [UIView animateWithDuration:(animated ? 0.2 : 0) animations:^{
68 | if (selected) {
69 | self.checkImage.tintColor = AGTheme.shared.tintColor;
70 | self.checkImage.image = [UIImage imageWithSymbol:@"checkmark.circle.fill"];
71 | } else {
72 | self.checkImage.tintColor = AGTheme.shared.minorLabelColor;
73 | self.checkImage.image = [UIImage imageWithSymbol:@"circle"];
74 | }
75 | }];
76 | }
77 | }
78 |
79 | - (void)setModel:(AGMFAModel *)model {
80 | if (_model != model) {
81 | _model = model;
82 | self.titleLabel.text = model.title;
83 | self.detailLabel.text = model.detail;
84 | self.createdLabel.created = model.created;
85 | if (!self.model.canExportPB) {
86 | self.checkImage.tintColor = AGTheme.shared.alertColor;
87 | self.checkImage.image = [UIImage imageWithSymbol:@"exclamationmark.circle.fill"];
88 | }
89 | }
90 | }
91 |
92 |
93 | @end
94 |
--------------------------------------------------------------------------------
/Watch Extension/Classes/ExtensionDelegate.m:
--------------------------------------------------------------------------------
1 | //
2 | // ExtensionDelegate.m
3 | // Watch Extension
4 | //
5 | // Created by WizJin on 2020/12/11.
6 | //
7 |
8 | #import "ExtensionDelegate.h"
9 |
10 | @implementation ExtensionDelegate
11 |
12 | - (void)applicationDidFinishLaunching {
13 | // Perform any final initialization of your application.
14 | }
15 |
16 | - (void)applicationDidBecomeActive {
17 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
18 | }
19 |
20 | - (void)applicationWillResignActive {
21 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
22 | // Use this method to pause ongoing tasks, disable timers, etc.
23 | }
24 |
25 | - (void)handleBackgroundTasks:(NSSet *)backgroundTasks {
26 | // Sent when the system needs to launch the application in the background to process tasks. Tasks arrive in a set, so loop through and process each one.
27 | for (WKRefreshBackgroundTask * task in backgroundTasks) {
28 | // Check the Class of each task to decide how to process it
29 | if ([task isKindOfClass:[WKApplicationRefreshBackgroundTask class]]) {
30 | // Be sure to complete the background task once you’re done.
31 | WKApplicationRefreshBackgroundTask *backgroundTask = (WKApplicationRefreshBackgroundTask*)task;
32 | [backgroundTask setTaskCompletedWithSnapshot:NO];
33 | } else if ([task isKindOfClass:[WKSnapshotRefreshBackgroundTask class]]) {
34 | // Snapshot tasks have a unique completion call, make sure to set your expiration date
35 | WKSnapshotRefreshBackgroundTask *snapshotTask = (WKSnapshotRefreshBackgroundTask*)task;
36 | [snapshotTask setTaskCompletedWithDefaultStateRestored:YES estimatedSnapshotExpiration:[NSDate distantFuture] userInfo:nil];
37 | } else if ([task isKindOfClass:[WKWatchConnectivityRefreshBackgroundTask class]]) {
38 | // Be sure to complete the background task once you’re done.
39 | WKWatchConnectivityRefreshBackgroundTask *backgroundTask = (WKWatchConnectivityRefreshBackgroundTask*)task;
40 | [backgroundTask setTaskCompletedWithSnapshot:NO];
41 | } else if ([task isKindOfClass:[WKURLSessionRefreshBackgroundTask class]]) {
42 | // Be sure to complete the background task once you’re done.
43 | WKURLSessionRefreshBackgroundTask *backgroundTask = (WKURLSessionRefreshBackgroundTask*)task;
44 | [backgroundTask setTaskCompletedWithSnapshot:NO];
45 | } else if ([task isKindOfClass:[WKRelevantShortcutRefreshBackgroundTask class]]) {
46 | // Be sure to complete the relevant-shortcut task once you’re done.
47 | WKRelevantShortcutRefreshBackgroundTask *relevantShortcutTask = (WKRelevantShortcutRefreshBackgroundTask*)task;
48 | [relevantShortcutTask setTaskCompletedWithSnapshot:NO];
49 | } else if ([task isKindOfClass:[WKIntentDidRunRefreshBackgroundTask class]]) {
50 | // Be sure to complete the intent-did-run task once you’re done.
51 | WKIntentDidRunRefreshBackgroundTask *intentDidRunTask = (WKIntentDidRunRefreshBackgroundTask*)task;
52 | [intentDidRunTask setTaskCompletedWithSnapshot:NO];
53 | } else {
54 | // make sure to complete unhandled task types
55 | [task setTaskCompletedWithSnapshot:NO];
56 | }
57 | }
58 | }
59 |
60 | @end
61 |
--------------------------------------------------------------------------------
/Argus/Models/AGMFAModel+GPB.m:
--------------------------------------------------------------------------------
1 | //
2 | // AGMFAModel+GPB.m
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/12/18.
6 | //
7 |
8 | #import "AGMFAModel+GPB.h"
9 | #import "AGModel.pbobjc.h"
10 |
11 | @implementation AGMFAModel (GPB)
12 |
13 | + (nullable NSString *)URLWithParams:(AGMOtpParameters *)params {
14 | NSURLComponents *components = [NSURLComponents new];
15 | components.scheme = @"otpauth";
16 | if (params.type == AGMOtpType_OtpTypeTotp) {
17 | components.host = @"totp";
18 | if (params.issuer.length <= 0) {
19 | components.path = [NSString stringWithFormat:@"/%@", params.name];
20 | } else {
21 | if (params.name.length <= 0) {
22 | components.path = [NSString stringWithFormat:@"/%@", params.name];
23 | } else {
24 | components.path = [NSString stringWithFormat:@"/%@:%@", params.issuer, params.name];
25 | }
26 | }
27 | NSMutableArray *items = [NSMutableArray new];
28 | switch (params.algorithm) {
29 | case AGMAlgorithm_AlgorithmSha1:
30 | break;
31 | case AGMAlgorithm_AlgorithmSha256:
32 | [items addObject:[NSURLQueryItem queryItemWithName:@"algorithm" value:@"sha256"]];
33 | break;
34 | case AGMAlgorithm_AlgorithmSha512:
35 | [items addObject:[NSURLQueryItem queryItemWithName:@"algorithm" value:@"sha512"]];
36 | break;
37 | case AGMAlgorithm_AlgorithmMd5:
38 | [items addObject:[NSURLQueryItem queryItemWithName:@"algorithm" value:@"md5"]];
39 | break;
40 | default:
41 | return nil;
42 | }
43 | switch (params.digits) {
44 | case AGMDigitCount_DigitCountSix:
45 | break;
46 | case AGMDigitCount_DigitCountEight:
47 | [items addObject:[NSURLQueryItem queryItemWithName:@"digits" value:@"8"]];
48 | break;
49 | default:
50 | return nil;
51 | }
52 | if (params.secret.length > 0) {
53 | [items addObject:[NSURLQueryItem queryItemWithName:@"secret" value:params.secret.base32EncodedString]];
54 | }
55 | if (params.issuer.length > 0) {
56 | [items addObject:[NSURLQueryItem queryItemWithName:@"issuer" value:params.issuer]];
57 | }
58 | components.queryItems = items;
59 | return components.URL.absoluteString;
60 | }
61 | return nil;
62 | }
63 |
64 | - (BOOL)calcCanExportPB {
65 | switch (self.algorithm) {
66 | default: return NO;
67 | case kCCHmacAlgSHA1:
68 | case kCCHmacAlgSHA256:
69 | case kCCHmacAlgSHA512:
70 | case kCCHmacAlgMD5: break;
71 | }
72 | switch (self.digits) {
73 | default: return NO;
74 | case 6: case 8: break;
75 | }
76 | if (self.period != 30) {
77 | return NO;
78 | }
79 | return YES;
80 | }
81 |
82 | - (AGMOtpParameters *)pbParams {
83 | AGMOtpParameters *item = nil;
84 | if (self.canExportPB) {
85 | item = [AGMOtpParameters new];
86 | item.type = AGMOtpType_OtpTypeTotp;
87 | item.issuer = self.title;
88 | item.name = self.detail;
89 | item.secret = self.secret;
90 | switch (self.algorithm) {
91 | case kCCHmacAlgSHA1: item.algorithm = AGMAlgorithm_AlgorithmSha1; break;
92 | case kCCHmacAlgSHA256: item.algorithm = AGMAlgorithm_AlgorithmSha256; break;
93 | case kCCHmacAlgSHA512: item.algorithm = AGMAlgorithm_AlgorithmSha512; break;
94 | case kCCHmacAlgMD5: item.algorithm = AGMAlgorithm_AlgorithmMd5; break;
95 | }
96 | switch (self.digits) {
97 | case 6: item.digits = AGMDigitCount_DigitCountSix; break;
98 | case 8: item.digits = AGMDigitCount_DigitCountEight; break;
99 | }
100 | }
101 | return item;
102 | }
103 |
104 |
105 | @end
106 |
--------------------------------------------------------------------------------
/Watch Extension/Classes/InterfaceController.m:
--------------------------------------------------------------------------------
1 | //
2 | // InterfaceController.m
3 | // Watch Extension
4 | //
5 | // Created by WizJin on 2020/12/11.
6 | //
7 |
8 | #import "InterfaceController.h"
9 | #import
10 | #import "CodeRowType.h"
11 | #import "AGMFAStorage.h"
12 | #import "AGDevice.h"
13 |
14 | @interface InterfaceController ()
15 |
16 | @property (weak, nonatomic) IBOutlet WKInterfaceLabel *emptyTitle;
17 | @property (weak, nonatomic) IBOutlet WKInterfaceLabel *emptyBody;
18 | @property (weak, nonatomic) IBOutlet WKInterfaceTable *codeTable;
19 |
20 | @property (nonatomic, readonly, strong) AGMFAStorage *storage;
21 | @property (nonatomic, readonly, strong) WCSession *session;
22 | @property (nonatomic, nullable, strong) NSTimer *refreshTimer;
23 |
24 | @end
25 |
26 |
27 | @implementation InterfaceController
28 |
29 | - (instancetype)init {
30 | if (self = [super init]) {
31 | _storage = [[AGMFAStorage alloc] initWithURL:[NSURL URLWithString:@kAGMFAFileName relativeToURL:AGDevice.shared.docdir]];
32 | _refreshTimer = nil;
33 | if (!WCSession.isSupported) {
34 | _session = nil;
35 | } else {
36 | _session = WCSession.defaultSession;
37 | self.session.delegate = self;
38 | [self.session activateSession];
39 | }
40 | }
41 | return self;
42 | }
43 |
44 | - (void)awakeWithContext:(id)context {
45 | [self.emptyTitle setText:@"NoMFATitle".localized];
46 | [self.emptyBody setText:@"NoMFAWatch".localized];
47 | [self.emptyTitle setHidden:NO];
48 | [self.emptyBody setHidden:NO];
49 | [self.codeTable setHidden:YES];
50 | }
51 |
52 | - (void)willActivate {
53 | [self updateContext];
54 | [self startRefreshTimer];
55 | }
56 |
57 | - (void)didDeactivate {
58 | [self stopRefreshTimer];
59 | }
60 |
61 | - (void)didAppear {
62 | [self startRefreshTimer];
63 | }
64 |
65 | - (void)willDisappear {
66 | [self stopRefreshTimer];
67 | }
68 |
69 | #pragma mark - WCSessionDelegate
70 | - (void)session:(WCSession *)session activationDidCompleteWithState:(WCSessionActivationState)activationState error:(nullable NSError *)error {
71 | [self updateContext];
72 | }
73 |
74 | - (void)session:(WCSession *)session didReceiveApplicationContext:(NSDictionary *)applicationContext {
75 | @weakify(self);
76 | dispatch_async(dispatch_get_main_queue(), ^{
77 | @strongify(self);
78 | [self updateContext];
79 | });
80 | }
81 |
82 | #pragma mark - Private Methods
83 | - (void)startRefreshTimer {
84 | if (self.refreshTimer == nil) {
85 | _refreshTimer = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(actionRefresh:) userInfo:nil repeats:YES];
86 | }
87 | }
88 |
89 | - (void)stopRefreshTimer {
90 | if (self.refreshTimer != nil) {
91 | [self.refreshTimer invalidate];
92 | _refreshTimer = nil;
93 | }
94 | }
95 |
96 | - (void)actionRefresh:(id)sender {
97 | time_t now = time(NULL);
98 | for(NSInteger i = 0; i < self.codeTable.numberOfRows; i++) {
99 | [[self.codeTable rowControllerAtIndex:i] update:now];
100 | }
101 | }
102 |
103 | - (void)updateContext {
104 | if (self.session != nil && self.session.receivedApplicationContext != nil) {
105 | id data = [self.session.receivedApplicationContext objectForKey:@"data"];
106 | if (data != nil && [data isKindOfClass:NSData.class]) {
107 | [self reloadData]; // TODO: fix cache
108 | if ([self.storage saveData:data]) {
109 | [self reloadData];
110 | }
111 | }
112 | }
113 | }
114 |
115 | - (void)reloadData {
116 | if (self.storage.changed && [self.storage load]) {
117 | if (self.storage.count <= 0) {
118 | [self.emptyTitle setHidden:NO];
119 | [self.emptyBody setHidden:NO];
120 | [self.codeTable setHidden:YES];
121 | } else {
122 | [self.emptyTitle setHidden:YES];
123 | [self.emptyBody setHidden:YES];
124 | [self.codeTable setHidden:NO];
125 | [self.codeTable setNumberOfRows:self.storage.count withRowType:@"CodeRowType"];
126 | for(NSInteger i = 0; i < self.codeTable.numberOfRows; i++) {
127 | [[self.codeTable rowControllerAtIndex:i] setModel:[self.storage itemAtIndex:i]];
128 | }
129 | }
130 | }
131 | }
132 |
133 |
134 | @end
135 |
--------------------------------------------------------------------------------
/Argus/Services/AGSecurity.m:
--------------------------------------------------------------------------------
1 | //
2 | // AGSecurity.m
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/12/10.
6 | //
7 |
8 | #import "AGSecurity.h"
9 | #import
10 | #import
11 | #import "AGDevice.h"
12 |
13 | #define kAGSecKeyCommon \
14 | (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword, \
15 | (__bridge id)kSecAttrAccount: AGDevice.shared.name, \
16 | (__bridge id)kSecAttrService: @"com.wizjin.argus.lock", \
17 |
18 | @interface AGSecurity ()
19 |
20 | @property (nonatomic, assign) time_t lastUpdate;
21 |
22 | @end
23 |
24 | @implementation AGSecurity
25 |
26 | + (instancetype)shared {
27 | static AGSecurity *security;
28 | static dispatch_once_t onceToken;
29 | dispatch_once(&onceToken, ^{
30 | security = [AGSecurity new];
31 | });
32 | return security;
33 | }
34 |
35 | - (instancetype)init {
36 | if (self = [super init]) {
37 | _lastUpdate = 0;
38 | _hasLocker = [NSUserDefaults.standardUserDefaults boolForKey:@"hasLocker"];
39 | if (_hasLocker && !self.isKeyExist) {
40 | _hasLocker = NO;
41 | [NSUserDefaults.standardUserDefaults setBool:_hasLocker forKey:@"hasLocker"];
42 | }
43 | }
44 | return self;
45 | }
46 |
47 | - (void)setHasLocker:(BOOL)hasLocker {
48 | if (self.hasLocker != hasLocker) {
49 | if (!hasLocker) {
50 | OSStatus err = SecItemDelete((__bridge CFDictionaryRef)@{ kAGSecKeyCommon });
51 | if (err != errSecSuccess && err != errSecItemNotFound) {
52 | return;
53 | }
54 | } else {
55 | if (!self.isKeyExist) {
56 | hasLocker = NO;
57 | LAContext *context = [LAContext new];
58 | context.touchIDAuthenticationAllowableReuseDuration = 10;
59 | NSMutableData *data = [NSMutableData dataWithLength:sizeof(uuid_t)];
60 | [NSUUID.UUID getUUIDBytes:data.mutableBytes];
61 | SecAccessControlRef access = SecAccessControlCreateWithFlags(NULL, kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly, kSecAccessControlUserPresence, NULL);
62 | if (access != NULL) {
63 | OSStatus err = SecItemAdd((__bridge CFDictionaryRef)@{
64 | kAGSecKeyCommon
65 | (__bridge id)kSecAttrAccessControl: (__bridge id)access,
66 | (__bridge id)kSecUseAuthenticationContext: context,
67 | (__bridge id)kSecValueData: data,
68 | }, NULL);
69 | if (err == errSecSuccess) {
70 | hasLocker = YES;
71 | }
72 | CFRelease(access);
73 | }
74 | }
75 | if (!hasLocker) {
76 | return;
77 | }
78 | }
79 | _hasLocker = hasLocker;
80 | [NSUserDefaults.standardUserDefaults setBool:hasLocker forKey:@"hasLocker"];
81 | }
82 | }
83 |
84 | - (BOOL)checkLocker {
85 | BOOL res = YES;
86 | if (self.isLocking) {
87 | res = NO;
88 | LAContext *context = [LAContext new];
89 | context.interactionNotAllowed = NO;
90 | context.touchIDAuthenticationAllowableReuseDuration = 10;
91 | context.localizedReason = @"Use password to unlock".localized;
92 | if (findSecItem(context) == errSecSuccess) {
93 | self.lastUpdate = time(NULL);
94 | res = YES;
95 | }
96 | }
97 | return res;
98 | }
99 |
100 | - (BOOL)isLocking {
101 | return (self.hasLocker && self.lastUpdate < time(NULL) - 10);
102 | }
103 |
104 | #pragma mark - Private Methods
105 | - (BOOL)isKeyExist {
106 | LAContext *context = [LAContext new];
107 | context.interactionNotAllowed = YES;
108 | OSStatus err = findSecItem(context);
109 | return (err == errSecSuccess || err == errSecInteractionNotAllowed);
110 | }
111 |
112 | static inline OSStatus findSecItem(LAContext *context) {
113 | return SecItemCopyMatching((__bridge CFDictionaryRef)@{
114 | kAGSecKeyCommon
115 | (__bridge id)kSecUseAuthenticationContext: context,
116 | (__bridge id)kSecMatchLimit: (__bridge id)kSecMatchLimitOne,
117 | (__bridge id)kSecReturnAttributes: (__bridge id)kCFBooleanFalse,
118 | (__bridge id)kSecReturnData: (__bridge id)kCFBooleanFalse,
119 | }, NULL);
120 | }
121 |
122 |
123 | @end
124 |
--------------------------------------------------------------------------------
/Argus/Views/Export/AGExportViewController.m:
--------------------------------------------------------------------------------
1 | //
2 | // AGExportViewController.m
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/12/18.
6 | //
7 |
8 | #import "AGExportViewController.h"
9 | #import "AGExportTableViewCell.h"
10 | #import "AGMFAManager.h"
11 | #import "AGRouter.h"
12 | #import "AGTheme.h"
13 |
14 | #define kCHExportItemMaxN 10
15 |
16 | static NSString *const cellIdentifier = @"cell";
17 |
18 | @interface AGExportViewController ()
19 |
20 | @property (nonatomic, readonly, strong) UITableView *tableView;
21 | @property (nonatomic, readonly, strong) NSHashTable *selectedItems;
22 |
23 | @end
24 |
25 | @implementation AGExportViewController
26 |
27 | - (instancetype)init {
28 | if (self = [super init]) {
29 | _selectedItems = [NSHashTable weakObjectsHashTable];
30 | }
31 | return self;
32 | }
33 |
34 | - (void)dealloc {
35 | [AGMFAManager.shared removeDelegate:self];
36 | }
37 |
38 | - (void)viewDidLoad {
39 | [super viewDidLoad];
40 | self.navigationItem.rightBarButtonItem = [UIBarButtonItem itemWithTitle:@"Export".localized target:self action:@selector(actionExport:)];
41 |
42 | UITableView *tableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain];
43 | [self.view addSubview:(_tableView = tableView)];
44 | [tableView registerClass:AGExportTableViewCell.class forCellReuseIdentifier:cellIdentifier];
45 | tableView.tableHeaderView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 0, kAGTableCellMargin)];
46 | tableView.backgroundColor = AGTheme.shared.groupedBackgroundColor;
47 | tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
48 | tableView.allowsMultipleSelection = YES;
49 | tableView.rowHeight = kAGExportCellHeight + kAGTableCellMargin;
50 | tableView.delegate = self;
51 | tableView.dataSource = self;
52 | [tableView mas_makeConstraints:^(MASConstraintMaker *make) {
53 | make.top.equalTo(self.view.mas_safeAreaLayoutGuideTop);
54 | make.left.right.bottom.equalTo(self.view);
55 | }];
56 |
57 | [AGMFAManager.shared addDelegate:self];
58 | [self updateAction];
59 | }
60 |
61 | #pragma mark - UITableViewDelegate
62 | - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
63 | AGMFAModel *model = [[tableView cellForRowAtIndexPath:indexPath] model];
64 | if (model != nil) {
65 | if (!model.canExportPB) {
66 | [tableView deselectRowAtIndexPath:indexPath animated:YES];
67 | [AGRouter.shared makeToast:@"This record format is not supported to export!".localized];
68 | } else {
69 | if ([self.selectedItems containsObject:model]) {
70 | [self.selectedItems removeObject:model];
71 | [tableView deselectRowAtIndexPath:indexPath animated:YES];
72 | } else if (self.selectedItems.count >= kCHExportItemMaxN) {
73 | [tableView deselectRowAtIndexPath:indexPath animated:YES];
74 | [AGRouter.shared makeToast:[NSString stringWithFormat:@"Export up to %@ records at a time.".localized, @(kCHExportItemMaxN)]];
75 | } else {
76 | [self.selectedItems addObject:model];
77 | }
78 | }
79 | [self updateAction];
80 | }
81 | }
82 |
83 | #pragma mark - UITableViewDataSource
84 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
85 | return AGMFAManager.shared.itemCount;
86 | }
87 |
88 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
89 | AGExportTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
90 | if (cell != nil) {
91 | cell.model = [AGMFAManager.shared itemAtIndex:indexPath.row];
92 | [cell setSelected:[self.selectedItems containsObject:cell.model] animated:NO];
93 | }
94 | return cell;
95 | }
96 |
97 | #pragma mark - AGMFAManagerDelegate
98 | - (void)mfaUpdated {
99 | [self.tableView reloadData];
100 | }
101 |
102 | #pragma mark - Action Methods
103 | - (void)actionExport:(id)sender {
104 | [AGRouter.shared routeTo:@"/page/export_qrcode" withParams:@{
105 | @"urls": [AGMFAManager.shared createExportURL:self.selectedItems.allObjects],
106 | }];
107 | }
108 |
109 | #pragma mark - Private Methods
110 | - (void)updateAction {
111 | self.navigationItem.rightBarButtonItem.enabled = (self.selectedItems.count > 0);
112 | self.title = [NSString stringWithFormat:@"%lu / %lu", (unsigned long)self.selectedItems.count, (unsigned long)AGMFAManager.shared.itemCount];
113 | }
114 |
115 |
116 | @end
117 |
--------------------------------------------------------------------------------
/Argus/Views/Export/AGExportQrcodeViewController.m:
--------------------------------------------------------------------------------
1 | //
2 | // AGExportQrcodeViewController.m
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/12/18.
6 | //
7 |
8 | #import "AGExportQrcodeViewController.h"
9 | #import "AGCreatedLabel.h"
10 | #import "AGQRCodeView.h"
11 | #import "AGRouter.h"
12 | #import "AGTheme.h"
13 |
14 | @interface AGExportQrcodeViewController ()
15 |
16 | @property (nonatomic, readonly, strong) NSString *url;
17 |
18 | @end
19 |
20 | @implementation AGExportQrcodeViewController
21 |
22 | - (instancetype)initWithParameters:(NSDictionary *)params {
23 | if (self = [super init]) {
24 | NSArray *urls = [params valueForKey:@"urls"];
25 | if (urls.count > 0) {
26 | _url = urls.firstObject;
27 | } else {
28 | _url = @"";
29 | }
30 | }
31 | return self;
32 | }
33 |
34 | - (void)viewDidLoad {
35 | [super viewDidLoad];
36 |
37 | AGTheme *theme = AGTheme.shared;
38 |
39 | self.navigationItem.rightBarButtonItem = [UIBarButtonItem itemWithImage:[UIImage imageWithSymbol:@"square.and.arrow.up"] target:self action:@selector(actionExport:)];
40 |
41 | UIScrollView *view = [UIScrollView new];
42 | [self.view addSubview:view];
43 | view.alwaysBounceVertical = YES;
44 | view.showsVerticalScrollIndicator = NO;
45 | view.showsHorizontalScrollIndicator = NO;
46 | view.backgroundColor = theme.backgroundColor;
47 | [view mas_makeConstraints:^(MASConstraintMaker *make) {
48 | make.top.equalTo(self.view.mas_safeAreaLayoutGuideTop);
49 | make.left.right.bottom.equalTo(self.view);
50 | }];
51 |
52 | CGSize size = UIScreen.mainScreen.bounds.size;
53 | size.width = MIN(MAX(MIN(size.width, size.height) - 60, 300), 600);
54 | size.height = size.width;
55 |
56 | AGQRCodeView *qrCodeView = [AGQRCodeView new];
57 | [view addSubview:qrCodeView];
58 | [qrCodeView mas_makeConstraints:^(MASConstraintMaker *make) {
59 | make.top.equalTo(view).offset(80);
60 | make.centerX.equalTo(view);
61 | make.size.mas_equalTo(size);
62 | }];
63 | qrCodeView.url = self.url;
64 |
65 | UILabel *titleLabel = [UILabel new];
66 | [view addSubview:titleLabel];
67 | [titleLabel mas_makeConstraints:^(MASConstraintMaker *make) {
68 | make.top.equalTo(qrCodeView.mas_bottom).offset(30);
69 | make.left.right.equalTo(qrCodeView);
70 | }];
71 | titleLabel.textColor = theme.labelColor;
72 | titleLabel.font = [UIFont systemFontOfSize:20];
73 | titleLabel.textAlignment = NSTextAlignmentCenter;
74 | titleLabel.text = @"TOTPInfoTitle".localized;
75 |
76 | UILabel *detailLabel = [UILabel new];
77 | [view addSubview:detailLabel];
78 | [detailLabel mas_makeConstraints:^(MASConstraintMaker *make) {
79 | make.top.equalTo(titleLabel.mas_bottom).offset(16);
80 | make.left.right.equalTo(titleLabel);
81 | }];
82 | detailLabel.numberOfLines = 0;
83 | NSMutableParagraphStyle *style = [NSMutableParagraphStyle new];
84 | style.alignment = NSTextAlignmentCenter;
85 | style.lineSpacing = 8;
86 | NSMutableAttributedString *text = [[NSMutableAttributedString alloc] initWithString:@"TOTPInfoDetail".localized];
87 | [text addAttributes:@{
88 | NSFontAttributeName: [UIFont systemFontOfSize:16],
89 | NSForegroundColorAttributeName: theme.labelColor,
90 | NSParagraphStyleAttributeName:style,
91 | } range:NSMakeRange(0, text.length)];
92 | detailLabel.attributedText = text;
93 |
94 | AGCreatedLabel *createdLabel = [AGCreatedLabel new];
95 | [view addSubview:createdLabel];
96 | [createdLabel mas_makeConstraints:^(MASConstraintMaker *make) {
97 | make.top.equalTo(view).offset(10);
98 | make.right.equalTo(qrCodeView);
99 | }];
100 | createdLabel.font = [UIFont systemFontOfSize:12];
101 | createdLabel.created = NSDate.date.timeIntervalSince1970*1000;
102 | }
103 |
104 | #pragma mark - Action Methods
105 | - (void)actionExport:(UIBarButtonItem *)sender {
106 | UIActivityViewController *vc = [[UIActivityViewController alloc] initWithActivityItems:@[self.view.snapshotImage, [NSURL URLWithString:self.url]] applicationActivities:nil];
107 | vc.completionWithItemsHandler = ^(UIActivityType activityType, BOOL completed, NSArray *returnedItems, NSError *activityError) {
108 | if (activityError != nil) {
109 | [AGRouter.shared makeToast:@"Export failed!".localized];
110 | } else if (completed) {
111 | [AGRouter.shared makeToast:@"Export success!".localized];
112 | }
113 | };
114 | if (UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPad) {
115 | vc.popoverPresentationController.barButtonItem = sender;
116 | }
117 | [AGRouter.shared presentViewController:vc animated:YES];
118 | }
119 |
120 |
121 | @end
122 |
--------------------------------------------------------------------------------
/Argus/Views/Main/AGMainViewController.m:
--------------------------------------------------------------------------------
1 | //
2 | // AGMainViewController.m
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/11/30.
6 | //
7 |
8 | #import "AGMainViewController.h"
9 | #import "AGMFATableViewCell.h"
10 | #import "AGMFATableView.h"
11 | #import "AGMFAEmptyView.h"
12 | #import "AGMFAManager.h"
13 | #import "AGRouter.h"
14 | #import "AGDevice.h"
15 | #import "AGTheme.h"
16 |
17 | static NSString *const cellIdentifier = @"cell";
18 |
19 | @interface AGMainViewController ()
20 |
21 | @property (nonatomic, readonly, strong) AGMFATableView *tableView;
22 | @property (nonatomic, readonly, strong) NSTimer *refreshTimer;
23 | @property (nonatomic, readonly, strong) NSHashTable *refreshItems;
24 |
25 | @end
26 |
27 | @implementation AGMainViewController
28 |
29 | - (instancetype)init {
30 | if (self = [super init]) {
31 | _refreshTimer = nil;
32 | _refreshItems = [NSHashTable weakObjectsHashTable];
33 | }
34 | return self;
35 | }
36 |
37 | - (void)dealloc {
38 | [AGMFAManager.shared removeDelegate:self];
39 | [self stopRefreshTimer];
40 | }
41 |
42 | - (void)viewDidLoad {
43 | [super viewDidLoad];
44 | self.title = AGDevice.shared.name;
45 |
46 | self.navigationItem.leftBarButtonItem = [UIBarButtonItem itemWithImage:[UIImage imageWithSymbol:@"gear"] target:self action:@selector(actionSettings:)];
47 | self.navigationItem.rightBarButtonItem = [UIBarButtonItem itemWithImage:[UIImage imageWithSymbol:@"qrcode.viewfinder"] target:self action:@selector(actionScan:)];
48 |
49 | AGMFATableView *tableView = [AGMFATableView new];
50 | [self.view addSubview:(_tableView = tableView)];
51 | [tableView registerClass:AGMFATableViewCell.class forCellReuseIdentifier:cellIdentifier];
52 | tableView.delegate = self;
53 | tableView.dataSource = self;
54 | [tableView mas_makeConstraints:^(MASConstraintMaker *make) {
55 | make.top.equalTo(self.view.mas_safeAreaLayoutGuideTop);
56 | make.left.right.bottom.equalTo(self.view);
57 | }];
58 |
59 | [AGMFAManager.shared addDelegate:self];
60 | }
61 |
62 | - (void)viewWillAppear:(BOOL)animated {
63 | [super viewWillAppear:animated];
64 | [self startRefreshTimer];
65 | }
66 |
67 | - (void)viewWillDisappear:(BOOL)animated {
68 | [self stopRefreshTimer];
69 | [super viewWillDisappear:animated];
70 | }
71 |
72 | #pragma mark - UITableViewDelegate
73 | - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
74 | [tableView deselectRowAtIndexPath:indexPath animated:YES];
75 | [AGMFAManager.shared copyToPasteboard:[AGMFAManager.shared itemAtIndex:indexPath.row]];
76 | }
77 |
78 | - (nullable UISwipeActionsConfiguration *)tableView:(UITableView *)tableView leadingSwipeActionsConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath {
79 | return [UISwipeActionsConfiguration configurationWithActions:@[[AGMFATableViewCell actionEdit:tableView indexPath:indexPath]]];
80 | }
81 |
82 | - (UISwipeActionsConfiguration *)tableView:(UITableView *)tableView trailingSwipeActionsConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath {
83 | return [UISwipeActionsConfiguration configurationWithActions:@[[AGMFATableViewCell actionDelete:tableView indexPath:indexPath]]];
84 | }
85 |
86 | - (UIView *)tableViewEmptyView:(UITableView *)tableView {
87 | return [[AGMFAEmptyView alloc] initWithTarget:self action:@selector(actionAdd:)];
88 | }
89 |
90 | #pragma mark - UITableViewDataSource
91 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
92 | return AGMFAManager.shared.itemCount;
93 | }
94 |
95 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
96 | AGMFATableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
97 | if (cell != nil) {
98 | cell.model = [AGMFAManager.shared itemAtIndex:indexPath.row];
99 | [self.refreshItems addObject:cell];
100 | }
101 | return cell;
102 | }
103 |
104 | #pragma mark - AGMFAManagerDelegate
105 | - (void)mfaUpdated {
106 | [self.tableView reloadData];
107 | }
108 |
109 | #pragma mark - Private Methods
110 | - (void)actionSettings:(id)sender {
111 | [AGRouter.shared routeTo:@"/page/settings"];
112 | }
113 |
114 | - (void)actionScan:(id)sender {
115 | [AGRouter.shared routeTo:@"/page/scan"];
116 | }
117 |
118 | - (void)actionRefresh:(id)sender {
119 | time_t now = time(NULL);
120 | for (AGMFATableViewCell *cell in self.refreshItems) {
121 | [cell update:now];
122 | }
123 | }
124 |
125 | - (void)actionAdd:(id)sender {
126 | [AGRouter.shared routeTo:@"/page/scan"];
127 | }
128 |
129 | - (void)startRefreshTimer {
130 | if (self.refreshTimer == nil) {
131 | _refreshTimer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(actionRefresh:) userInfo:nil repeats:YES];
132 | }
133 | }
134 |
135 | - (void)stopRefreshTimer {
136 | if (self.refreshTimer != nil) {
137 | [self.refreshTimer invalidate];
138 | _refreshTimer = nil;
139 | }
140 | }
141 |
142 |
143 | @end
144 |
--------------------------------------------------------------------------------
/Argus/Models/AGMFAModel.m:
--------------------------------------------------------------------------------
1 | //
2 | // AGMFAModel.m
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/11/30.
6 | //
7 |
8 | #import "AGMFAModel.h"
9 | #import "AGMFAModel+GPB.h"
10 |
11 | @interface AGMFAModel () {
12 | @private
13 | size_t hashlen;
14 | }
15 |
16 | @property (nonatomic, strong) NSDictionary *data;
17 | @property (nonatomic, assign) uint64_t created;
18 | @property (nonatomic, assign) BOOL canExportPB;
19 |
20 | @end
21 |
22 | @implementation AGMFAModel
23 |
24 | + (instancetype)modelWithData:(NSDictionary *)data {
25 | uint64_t ts = [[data valueForKey:@"created"] longLongValue];
26 | if (ts > 0) {
27 | NSString *url = [data valueForKey:@"url"];
28 | if (url.length > 0) {
29 | NSURLComponents *componemts = [NSURLComponents componentsWithString:url];
30 | if ([componemts.scheme isEqualToString:@"otpauth"] && [componemts.host isEqualToString:@"totp"]) {
31 | AGMFAModel *model = [[self.class alloc] initWithURLComponents:componemts];
32 | if (model != nil) {
33 | model.created = ts;
34 | model.data = data;
35 | return model;
36 | }
37 | }
38 | }
39 | }
40 | return nil;
41 | }
42 |
43 | - (instancetype)initWithURLComponents:(NSURLComponents *)componemts {
44 | if (self = [super init]) {
45 | _created = 0;
46 |
47 | // Note: https://github.com/google/google-authenticator/wiki/Key-Uri-Format
48 | _digits = 6;
49 | _period = 0;
50 | hashlen = CC_SHA1_DIGEST_LENGTH;
51 | _algorithm = kCCHmacAlgSHA1;
52 | _title = @"";
53 | _detail = @"";
54 | if (componemts.path.length > 1) {
55 | NSString *path = [componemts.path substringFromIndex:1];
56 | NSRange range = [path rangeOfString:@":" options:0];
57 | if (range.location == NSNotFound) {
58 | _detail = path;
59 | } else {
60 | _title = [path substringToIndex:range.location].trim;
61 | if (range.location + 1 < path.length) {
62 | _detail = [path substringFromIndex:range.location + 1].trim;
63 | }
64 | }
65 | }
66 | for (NSURLQueryItem *item in componemts.queryItems) {
67 | if ([item.name isEqualToString:@"issuer"]) {
68 | _title = item.value;
69 | } else if ([item.name isEqualToString:@"secret"]) {
70 | _secret = [NSData dataWithBase32EncodedString:item.value];
71 | } else if ([item.name isEqualToString:@"period"]) {
72 | _period = [item.value intValue];
73 | } else if ([item.name isEqualToString:@"digits"]) {
74 | _digits = [item.value integerValue];
75 | } else if ([item.name isEqualToString:@"algorithm"]) {
76 | if ([item.value caseInsensitiveCompare:@"SHA1"] == NSOrderedSame) {
77 | hashlen = CC_SHA1_DIGEST_LENGTH;
78 | _algorithm = kCCHmacAlgSHA1;
79 | } else if ([item.value caseInsensitiveCompare:@"SHA224"] == NSOrderedSame) {
80 | hashlen = CC_SHA224_DIGEST_LENGTH;
81 | _algorithm = kCCHmacAlgSHA224;
82 | } else if ([item.value caseInsensitiveCompare:@"SHA256"] == NSOrderedSame) {
83 | hashlen = CC_SHA256_DIGEST_LENGTH;
84 | _algorithm = kCCHmacAlgSHA256;
85 | } else if ([item.value caseInsensitiveCompare:@"SHA384"] == NSOrderedSame) {
86 | hashlen = CC_SHA384_DIGEST_LENGTH;
87 | _algorithm = kCCHmacAlgSHA384;
88 | } else if ([item.value caseInsensitiveCompare:@"SHA512"] == NSOrderedSame) {
89 | hashlen = CC_SHA512_DIGEST_LENGTH;
90 | _algorithm = kCCHmacAlgSHA512;
91 | } else if ([item.value caseInsensitiveCompare:@"MD5"] == NSOrderedSame) {
92 | hashlen = CC_MD5_DIGEST_LENGTH;
93 | _algorithm = kCCHmacAlgMD5;
94 | }
95 | }
96 | }
97 | if (_secret == nil) _secret = [NSData new];
98 | if (_period <= 0) _period = 30;
99 | if (_digits <= 0 || _digits > 8) _digits = 6;
100 |
101 | if ([self respondsToSelector:@selector(calcCanExportPB)]) {
102 | _canExportPB = [self calcCanExportPB];
103 | } else {
104 | _canExportPB = NO;
105 | }
106 | }
107 | return self;
108 | }
109 |
110 | - (BOOL)isEqual:(AGMFAModel *)other {
111 | return (self.created == other.created || [self.url isEqual:other.url]
112 | || ([self.secret isEqualToData:other.secret] && [self.title isEqualToString:other.title] && [self.detail isEqualToString:other.detail] && self.period == other.period));
113 | }
114 |
115 | - (uint64_t)calcT:(time_t)now remainder:(nullable uint64_t *)remainder {
116 | uint64_t t = floor((double)now/self.period);
117 | if (remainder != NULL) *remainder = (t + 1) * self.period - now;
118 | return t;
119 | }
120 |
121 | - (NSString *)calcCode:(uint64_t)t {
122 | uint8_t hmac[128];
123 | assert(hashlen <= sizeof(hmac));
124 | t = CFSwapInt64BigToHost(t);
125 | CCHmac(self.algorithm, self.secret.bytes, self.secret.length, &t, sizeof(t), hmac);
126 | uint8_t offset = hmac[hashlen - 1] & 0x0f;
127 | int64_t value = ((hmac[offset] & 0x7f) << 24)
128 | | ((hmac[offset+1] & 0xff) << 16)
129 | | ((hmac[offset+2] & 0xff) << 8)
130 | | (hmac[offset+3] & 0xff);
131 | unichar *res = malloc((size_t)self.digits * sizeof(unichar));
132 | if (res != NULL) {
133 | for (int i = 0; i < self.digits; i++) {
134 | res[self.digits-i-1] = value%10 + '0';
135 | value /= 10;
136 | }
137 | return [[NSString alloc] initWithCharactersNoCopy:res length:(size_t)self.digits freeWhenDone:YES];
138 | }
139 | return @"";
140 | }
141 |
142 | - (NSString *)url {
143 | return [self.data valueForKey:@"url"];
144 | }
145 |
146 | @end
147 |
--------------------------------------------------------------------------------
/Argus/Views/Editor/AGEditorViewController.m:
--------------------------------------------------------------------------------
1 | //
2 | // AGEditorViewController.m
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/11/30.
6 | //
7 |
8 | #import "AGEditorViewController.h"
9 | #import "AGCountdownView.h"
10 | #import "AGCreatedLabel.h"
11 | #import "AGQRCodeView.h"
12 | #import "AGCodeView.h"
13 | #import "AGMFAManager.h"
14 | #import "AGRouter.h"
15 | #import "AGTheme.h"
16 |
17 | @interface AGEditorViewController ()
18 |
19 | @property (nonatomic, readonly, strong) NSTimer *refreshTimer;
20 | @property (nonatomic, readonly, strong) AGMFAModel *model;
21 | @property (nonatomic, readonly, strong) AGCountdownView * countdown;
22 | @property (nonatomic, readonly, strong) AGCodeView *codeLabel;
23 | @property (nonatomic, readonly, strong) AGQRCodeView *qrCodeView;
24 | @property (nonatomic, readonly, strong) UITapGestureRecognizer *recognizer;
25 |
26 | @end
27 |
28 | @implementation AGEditorViewController
29 |
30 | - (instancetype)initWithModel:(AGMFAModel *)model {
31 | if (self = [super init]) {
32 | _model = model;
33 | self.title = model.title;
34 | }
35 | return self;
36 | }
37 |
38 | - (void)dealloc {
39 | [self stopRefreshTimer];
40 | [self.view removeGestureRecognizer:self.recognizer];
41 | }
42 |
43 | - (void)viewDidLoad {
44 | [super viewDidLoad];
45 |
46 | AGTheme *theme = AGTheme.shared;
47 |
48 | self.navigationItem.rightBarButtonItem = [UIBarButtonItem itemWithImage:[UIImage imageWithSymbol:@"square.and.arrow.up"] target:self action:@selector(actionExport:)];
49 |
50 | UIScrollView *view = [UIScrollView new];
51 | [self.view addSubview:view];
52 | view.alwaysBounceVertical = YES;
53 | view.showsVerticalScrollIndicator = NO;
54 | view.showsHorizontalScrollIndicator = NO;
55 | view.backgroundColor = theme.groupedBackgroundColor;
56 | [view mas_makeConstraints:^(MASConstraintMaker *make) {
57 | make.top.equalTo(self.view.mas_safeAreaLayoutGuideTop);
58 | make.left.right.bottom.equalTo(self.view);
59 | }];
60 |
61 | UILabel *detailLabel = [UILabel new];
62 | [view addSubview:detailLabel];
63 | [detailLabel mas_makeConstraints:^(MASConstraintMaker *make) {
64 | make.top.equalTo(view).offset(20);
65 | make.left.equalTo(view).offset(20);
66 | }];
67 | detailLabel.font = [UIFont boldSystemFontOfSize:18];
68 | detailLabel.textColor = theme.labelColor;
69 | detailLabel.text = self.model.detail;
70 |
71 | AGCountdownView * countdown = [AGCountdownView new];
72 | [view addSubview:(_countdown = countdown)];
73 | [countdown mas_makeConstraints:^(MASConstraintMaker *make) {
74 | make.size.mas_equalTo(CGSizeMake(20, 20));
75 | make.top.equalTo(detailLabel);
76 | make.right.equalTo(self.view).offset(-20);
77 | }];
78 |
79 | AGCodeView *codeLabel = [AGCodeView new];
80 | [view addSubview:(_codeLabel = codeLabel)];
81 | [codeLabel mas_makeConstraints:^(MASConstraintMaker *make) {
82 | make.top.equalTo(detailLabel.mas_bottom).offset(40);
83 | make.centerX.equalTo(self.view);
84 | }];
85 | codeLabel.fontSize = 60;
86 |
87 | AGQRCodeView *qrCodeView = [AGQRCodeView new];
88 | [view addSubview:(_qrCodeView = qrCodeView)];
89 | [qrCodeView mas_makeConstraints:^(MASConstraintMaker *make) {
90 | make.top.equalTo(codeLabel.mas_bottom).offset(40);
91 | make.centerX.equalTo(codeLabel);
92 | make.size.mas_equalTo(CGSizeMake(260, 260));
93 | }];
94 | qrCodeView.url = self.model.url;
95 |
96 | AGCreatedLabel *createdLabel = [AGCreatedLabel new];
97 | [view addSubview:createdLabel];
98 | [createdLabel mas_makeConstraints:^(MASConstraintMaker *make) {
99 | make.top.equalTo(qrCodeView.mas_bottom).offset(30);
100 | make.centerX.equalTo(qrCodeView);
101 | }];
102 | createdLabel.font = [UIFont systemFontOfSize:12];
103 | createdLabel.created = self.model.created;
104 |
105 | UITapGestureRecognizer *recognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(actionDoCopy:)];
106 | [self.view addGestureRecognizer:(_recognizer = recognizer)];
107 | }
108 |
109 | - (void)viewWillAppear:(BOOL)animated {
110 | [super viewWillAppear:animated];
111 | [self startRefreshTimer];
112 | }
113 |
114 | - (void)viewWillDisappear:(BOOL)animated {
115 | [self stopRefreshTimer];
116 | [super viewWillDisappear:animated];
117 | }
118 |
119 | #pragma mark - Actions Methods
120 | - (void)actionExport:(UIBarButtonItem *)sender {
121 | UIActivityViewController *vc = [[UIActivityViewController alloc] initWithActivityItems:@[self.qrCodeView.snapshotImage, [NSURL URLWithString:self.model.url]] applicationActivities:nil];
122 | vc.completionWithItemsHandler = ^(UIActivityType activityType, BOOL completed, NSArray *returnedItems, NSError *activityError) {
123 | if (activityError != nil) {
124 | [AGRouter.shared makeToast:@"Export failed!".localized];
125 | } else if (completed) {
126 | [AGRouter.shared makeToast:@"Export success!".localized];
127 | }
128 | };
129 | if (UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPad) {
130 | vc.popoverPresentationController.barButtonItem = sender;
131 | }
132 | [AGRouter.shared presentViewController:vc animated:YES];
133 | }
134 |
135 | - (void)actionDoCopy:(id)sender {
136 | [AGMFAManager.shared copyToPasteboard:self.model];
137 | }
138 |
139 | - (void)actionRefresh:(id)sender {
140 | [self update:time(NULL)];
141 | }
142 |
143 | - (void)update:(time_t)now {
144 | [self.countdown update:self.model remainder:[self.codeLabel update:self.model now:now]];
145 | }
146 |
147 | #pragma mark - Private Methods
148 | - (void)startRefreshTimer {
149 | if (self.refreshTimer == nil) {
150 | _refreshTimer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(actionRefresh:) userInfo:nil repeats:YES];
151 | }
152 | }
153 |
154 | - (void)stopRefreshTimer {
155 | if (self.refreshTimer != nil) {
156 | [self.refreshTimer invalidate];
157 | _refreshTimer = nil;
158 | }
159 | }
160 |
161 |
162 | @end
163 |
--------------------------------------------------------------------------------
/Watch Extension/Classes/ComplicationController.m:
--------------------------------------------------------------------------------
1 | //
2 | // ComplicationController.m
3 | // Watch Extension
4 | //
5 | // Created by WizJin on 2020/12/11.
6 | //
7 |
8 | #import "ComplicationController.h"
9 | #import "Theme.h"
10 |
11 | @implementation ComplicationController
12 |
13 | #pragma mark - Complication Configuration
14 |
15 | - (void)getComplicationDescriptorsWithHandler:(void (^)(NSArray * _Nonnull))handler {
16 | NSMutableArray *families = [NSMutableArray arrayWithArray:CLKAllComplicationFamilies()];
17 | [families removeObject:@(CLKComplicationFamilyModularLarge)];
18 | [families removeObject:@(CLKComplicationFamilyUtilitarianLarge)];
19 | [families removeObject:@(CLKComplicationFamilyExtraLarge)];
20 | [families removeObject:@(CLKComplicationFamilyGraphicExtraLarge)];
21 | NSArray *descriptors = @[
22 | [[CLKComplicationDescriptor alloc] initWithIdentifier:@"complication"
23 | displayName:@"Argus"
24 | supportedFamilies:families]
25 | // Multiple complication support can be added here with more descriptors
26 | ];
27 |
28 | // Call the handler with the currently supported complication descriptors
29 | handler(descriptors);
30 | }
31 |
32 | - (void)handleSharedComplicationDescriptors:(NSArray *)complicationDescriptors {
33 | // Do any necessary work to support these newly shared complication descriptors
34 | }
35 |
36 | #pragma mark - Timeline Configuration
37 |
38 | - (void)getTimelineEndDateForComplication:(CLKComplication *)complication withHandler:(void(^)(NSDate * __nullable date))handler {
39 | handler(nil);
40 | }
41 |
42 | - (void)getPrivacyBehaviorForComplication:(CLKComplication *)complication withHandler:(void(^)(CLKComplicationPrivacyBehavior privacyBehavior))handler {
43 | // Call the handler with your desired behavior when the device is locked
44 | handler(CLKComplicationPrivacyBehaviorShowOnLockScreen);
45 | }
46 |
47 | #pragma mark - Timeline Population
48 |
49 | - (void)getCurrentTimelineEntryForComplication:(CLKComplication *)complication withHandler:(void(^)(CLKComplicationTimelineEntry * __nullable))handler {
50 | [self getLocalizableSampleTemplateForComplication:complication withHandler:^(CLKComplicationTemplate *_Nullable complicationTemplate) {
51 | if (complicationTemplate == nil) {
52 | handler(nil);
53 | } else {
54 | CLKComplicationTimelineEntry *entry = [CLKComplicationTimelineEntry new];
55 | entry.complicationTemplate = complicationTemplate;
56 | entry.date = NSDate.now;
57 | handler(entry);
58 | }
59 | }];
60 | }
61 |
62 | //- (void)getTimelineEntriesForComplication:(CLKComplication *)complication afterDate:(NSDate *)date limit:(NSUInteger)limit withHandler:(void(^)(NSArray * __nullable entries))handler {
63 | // // Call the handler with the timeline entries after the given date
64 | // handler(nil);
65 | //}
66 |
67 | #pragma mark - Sample Templates
68 |
69 | - (void)getLocalizableSampleTemplateForComplication:(CLKComplication *)complication withHandler:(void(^)(CLKComplicationTemplate * __nullable complicationTemplate))handler {
70 | switch (complication.family) {
71 | case CLKComplicationFamilyModularSmall:
72 | handler([CLKComplicationTemplateModularSmallSimpleImage templateWithImageProvider:[self imageProvider:@"Modular"]]);
73 | break;
74 | case CLKComplicationFamilyModularLarge:
75 | handler(nil);
76 | break;
77 | case CLKComplicationFamilyUtilitarianSmall:
78 | handler([CLKComplicationTemplateUtilitarianSmallSquare templateWithImageProvider:[self imageProvider:@"Utilitarian"]]);
79 | break;
80 | case CLKComplicationFamilyUtilitarianSmallFlat:
81 | handler([CLKComplicationTemplateUtilitarianSmallFlat templateWithTextProvider:[CLKTextProvider textProviderWithFormat:@"Argus"] imageProvider:[self imageProvider:@"Utilitarian"]]);
82 | break;
83 | case CLKComplicationFamilyUtilitarianLarge:
84 | handler(nil);
85 | break;
86 | case CLKComplicationFamilyCircularSmall:
87 | handler([CLKComplicationTemplateCircularSmallSimpleImage templateWithImageProvider:[self imageProvider:@"Circular"]]);
88 | break;
89 | case CLKComplicationFamilyExtraLarge:
90 | handler(nil);
91 | break;
92 | case CLKComplicationFamilyGraphicCorner:
93 | handler([CLKComplicationTemplateGraphicCornerTextImage templateWithTextProvider:[CLKTextProvider textProviderWithFormat:@"Argus"] imageProvider:[self imageColorProvider:@"Graphic Corner"]]);
94 | break;
95 | case CLKComplicationFamilyGraphicBezel:
96 | handler([CLKComplicationTemplateGraphicBezelCircularText templateWithCircularTemplate:[CLKComplicationTemplateGraphicCircularImage templateWithImageProvider:[self imageColorProvider:@"Graphic Circular"]]]);
97 | break;
98 | case CLKComplicationFamilyGraphicCircular:
99 | handler([CLKComplicationTemplateGraphicCircularImage templateWithImageProvider:[self imageColorProvider:@"Graphic Circular"]]);
100 | break;
101 | case CLKComplicationFamilyGraphicRectangular:
102 | handler([CLKComplicationTemplateGraphicRectangularFullImage templateWithImageProvider:[self imageColorProvider:@"Graphic Circular"]]);
103 | break;
104 | case CLKComplicationFamilyGraphicExtraLarge:
105 | handler(nil);
106 | break;
107 | }
108 | }
109 |
110 | #pragma mark - Private Mehods
111 | - (CLKImageProvider *)imageProvider:(NSString *)name {
112 | CLKImageProvider *provider = [CLKImageProvider imageProviderWithOnePieceImage:[UIImage imageNamed:[NSString stringWithFormat:@"Complication/%@", name]]];
113 | return provider;
114 | }
115 |
116 | - (CLKFullColorImageProvider *)imageColorProvider:(NSString *)name {
117 | return [CLKFullColorImageProvider providerWithFullColorImage:[UIImage imageNamed:[NSString stringWithFormat:@"Complication/%@", name]]];
118 | }
119 |
120 |
121 | @end
122 |
--------------------------------------------------------------------------------
/Watch/Base.lproj/Interface.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
26 |
30 |
31 |
32 |
36 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
59 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
--------------------------------------------------------------------------------
/Argus/Views/Main/AGMFATableViewCell.m:
--------------------------------------------------------------------------------
1 | //
2 | // AGMFATableViewCell.m
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/11/30.
6 | //
7 |
8 | #import "AGMFATableViewCell.h"
9 | #import "AGEditorViewController.h"
10 | #import "AGCountdownView.h"
11 | #import "AGCreatedLabel.h"
12 | #import "AGCodeView.h"
13 | #import "AGMFAManager.h"
14 | #import "AGRouter.h"
15 | #import "AGTheme.h"
16 |
17 | @interface AGMFATableViewCell ()
18 |
19 | @property (nonatomic, readonly, strong) UILabel *titleLabel;
20 | @property (nonatomic, readonly, strong) UILabel *detailLabel;
21 | @property (nonatomic, readonly, strong) AGCreatedLabel *createdLabel;
22 | @property (nonatomic, readonly, strong) AGCodeView *codeLabel;
23 | @property (nonatomic, readonly, strong) AGCountdownView * countdown;
24 |
25 | @end
26 |
27 | @implementation AGMFATableViewCell
28 |
29 | - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
30 | if ([super initWithStyle:style reuseIdentifier:reuseIdentifier]) {
31 | AGTheme *theme = AGTheme.shared;
32 |
33 | UILabel *titleLabel = [UILabel new];
34 | [self.mainView addSubview:(_titleLabel = titleLabel)];
35 | [titleLabel mas_makeConstraints:^(MASConstraintMaker *make) {
36 | make.top.equalTo(self.mainView).offset(10);
37 | make.left.equalTo(self.mainView).offset(12);
38 | }];
39 | titleLabel.font = [UIFont boldSystemFontOfSize:14];
40 | titleLabel.textColor = theme.labelColor;
41 |
42 | UILabel *detailLabel = [UILabel new];
43 | [self.mainView addSubview:(_detailLabel = detailLabel)];
44 | [detailLabel mas_makeConstraints:^(MASConstraintMaker *make) {
45 | make.bottom.equalTo(self.mainView).offset(-10);
46 | make.left.equalTo(titleLabel);
47 | }];
48 | detailLabel.font = [UIFont systemFontOfSize:12];
49 | detailLabel.textColor = theme.labelColor;
50 |
51 | AGCreatedLabel *createdLabel = [AGCreatedLabel new];
52 | [self.mainView addSubview:(_createdLabel = createdLabel)];
53 | [createdLabel mas_makeConstraints:^(MASConstraintMaker *make) {
54 | make.top.equalTo(titleLabel);
55 | make.right.equalTo(self.mainView).offset(-12);
56 | }];
57 |
58 | AGCountdownView * countdown = [AGCountdownView new];
59 | [self.mainView addSubview:(_countdown = countdown)];
60 | [countdown mas_makeConstraints:^(MASConstraintMaker *make) {
61 | make.size.mas_equalTo(CGSizeMake(20, 20));
62 | make.right.equalTo(createdLabel);
63 | make.bottom.equalTo(detailLabel);
64 | }];
65 |
66 | AGCodeView *codeLabel = [AGCodeView new];
67 | [self.mainView addSubview:(_codeLabel = codeLabel)];
68 | [codeLabel mas_makeConstraints:^(MASConstraintMaker *make) {
69 | make.centerY.equalTo(self.mainView);
70 | make.left.equalTo(titleLabel);
71 | }];
72 | codeLabel.fontSize = 50;
73 |
74 | _model = nil;
75 | }
76 | return self;
77 | }
78 |
79 | + (UIContextualAction *)actionEdit:(UITableView *)tableView indexPath:(NSIndexPath *)indexPath {
80 | UIContextualAction *action = [UIContextualAction contextualActionWithStyle:UIContextualActionStyleNormal title:nil handler:^(UIContextualAction *action, UIView *sourceView, void (^completionHandler)(BOOL)) {
81 | //NSIndexPath *indexPath = [sourceView.superview valueForKeyPath:@"_delegate._indexPath"];
82 | AGMFAModel *model = [[tableView cellForRowAtIndexPath:indexPath] model];
83 | if (model != nil) {
84 | [AGRouter.shared showViewController:[[AGEditorViewController alloc] initWithModel:model] animated:YES];
85 | }
86 | completionHandler(YES);
87 | }];
88 | action.backgroundColor = AGTheme.shared.infoColor;
89 | action.image = [UIImage imageWithSymbol:@"qrcode" height:26];
90 | return action;
91 | }
92 |
93 | + (UIContextualAction *)actionDelete:(UITableView *)tableView indexPath:(NSIndexPath *)indexPath {
94 | UIContextualAction *action = [UIContextualAction contextualActionWithStyle:UIContextualActionStyleDestructive title:nil handler:^(UIContextualAction *action, UIView *sourceView, void (^completionHandler)(BOOL)) {
95 | //NSIndexPath *indexPath = [sourceView.superview valueForKeyPath:@"_delegate._indexPath"];
96 | AGMFAModel *model = [[tableView cellForRowAtIndexPath:indexPath] model];
97 | if (model != nil) {
98 | UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Delete this record will NOT turn off OTP verification".localized message:@"" preferredStyle:UIAlertControllerStyleAlert];
99 | UIAlertAction* cancelAction = [UIAlertAction actionWithTitle:@"Cancel".localized style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) {}];
100 | UIAlertAction* deleteAction = [UIAlertAction actionWithTitle:@"Delete".localized style:UIAlertActionStyleDestructive
101 | handler:^(UIAlertAction * action) {
102 | [AGMFAManager.shared deleteItem:model completion:^{
103 | AGMFAModel *project = [[tableView cellForRowAtIndexPath:indexPath] model];
104 | if (project.created == model.created) {
105 | [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
106 | }
107 | }];
108 | }];
109 | [alert addAction:cancelAction];
110 | [alert addAction:deleteAction];
111 | [AGRouter.shared presentViewController:alert animated:YES];
112 | }
113 | completionHandler(YES);
114 | }];
115 | action.image = [UIImage imageWithSymbol:@"trash.fill" height:26];
116 | return action;
117 | }
118 |
119 | - (void)setModel:(AGMFAModel *)model {
120 | if (self.model != model) {
121 | _model = model;
122 | [self.codeLabel reset];
123 | self.titleLabel.text = model.title;
124 | self.detailLabel.text = model.detail;
125 | self.createdLabel.created = model.created;
126 | [self update:time(NULL)];
127 | }
128 | }
129 |
130 | - (void)update:(time_t)now {
131 | [self.countdown update:self.model remainder:[self.codeLabel update:self.model now:now]];
132 | }
133 |
134 |
135 | @end
136 |
137 |
--------------------------------------------------------------------------------
/Argus/Views/Web/AGWebViewRefresher.m:
--------------------------------------------------------------------------------
1 | //
2 | // AGWebViewRefresher.m
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/11/30.
6 | //
7 |
8 | #import "AGWebViewRefresher.h"
9 | #import "AGTheme.h"
10 |
11 | #define kRefreshButton 46
12 | #define kRefreshLogoSize 24
13 | #define kRefreshMaxHeight 160
14 |
15 | @interface AGWebViewRefresher ()
16 |
17 | @property (nonatomic, readonly, strong) UIImageView *secureIcon;
18 | @property (nonatomic, readonly, strong) UILabel *hostLabel;
19 | @property (nonatomic, readonly, strong) UILabel *tipsLabel;
20 | @property (nonatomic, readonly, strong) UIImageView *refreshIcon;
21 | @property (nonatomic, readonly, strong) UIView *refreshIconBG;
22 | @property (nonatomic, readonly, assign) CGFloat activeOffset;
23 |
24 | @end
25 |
26 | @implementation AGWebViewRefresher
27 |
28 | - (instancetype)init {
29 | if (self = [super init]) {
30 | AGTheme *theme = AGTheme.shared;
31 | _activeOffset = MAXFLOAT;
32 | self.backgroundColor = theme.backgroundColor;
33 | self.tintColor = UIColor.clearColor;
34 |
35 | UIImageView *secureIcon = [[UIImageView alloc] initWithImage:[UIImage imageWithSymbol:@"exclamationmark.triangle.fill"]];
36 | [self addSubview:(_secureIcon = secureIcon)];
37 | secureIcon.frame = CGRectMake(0, 0, 10, 10);
38 | secureIcon.contentMode = UIViewContentModeScaleAspectFit;
39 | secureIcon.tintColor = theme.warnColor;
40 | secureIcon.alpha = 0;
41 |
42 | UILabel *hostLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 10, 0, 16)];
43 | [self addSubview:(_hostLabel = hostLabel)];
44 | hostLabel.font = [UIFont boldSystemFontOfSize:12];
45 | hostLabel.textColor = theme.minorLabelColor;
46 | hostLabel.textAlignment = NSTextAlignmentCenter;
47 | hostLabel.numberOfLines = 1;
48 | hostLabel.alpha = 0;
49 |
50 | UILabel *tipsLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 0, 12)];
51 | [self addSubview:(_tipsLabel = tipsLabel)];
52 | tipsLabel.font = [UIFont systemFontOfSize:10];
53 | tipsLabel.textColor = theme.minorLabelColor;
54 | tipsLabel.textAlignment = NSTextAlignmentCenter;
55 | tipsLabel.numberOfLines = 1;
56 | tipsLabel.alpha = 0;
57 | tipsLabel.text = @"Release to reload".localized;
58 |
59 | UIView *refreshIconBG = [[UIView alloc] initWithFrame:CGRectZero];
60 | [self addSubview:(_refreshIconBG = refreshIconBG)];
61 | refreshIconBG.backgroundColor = [theme.labelColor colorWithAlphaComponent:0.2];
62 |
63 | UIImage *icon = [UIImage imageWithSymbol:@"arrow.clockwise"];
64 | if (@available(iOS 13.0, *)) {
65 | } else {
66 | icon = [icon resizeWithHeight:18];
67 | }
68 | UIImageView *refreshIcon = [[UIImageView alloc] initWithImage:icon];
69 | [self addSubview:(_refreshIcon = refreshIcon)];
70 | refreshIcon.tintColor = theme.labelColor;
71 | refreshIcon.alpha = 0;
72 | }
73 | return self;
74 | }
75 |
76 | - (void)layoutSubviews {
77 | [super layoutSubviews];
78 |
79 | CGFloat width = self.bounds.size.width;
80 | CGFloat height = -self.scrollView.contentOffset.y;
81 | CGFloat yoffset = 0;
82 |
83 | CGFloat iconWidth = self.secureIcon.bounds.size.width;
84 |
85 | CGRect frame = self.hostLabel.frame;
86 | CGFloat margin = frame.origin.y;
87 | CGSize size = [self.hostLabel sizeThatFits:CGSizeMake(width, frame.size.height)];
88 | frame.size.width = MIN(size.width, width * 0.8);
89 | frame.origin.x = (width - size.width + iconWidth - 1) * 0.5;
90 | self.hostLabel.frame = frame;
91 | yoffset += CGRectGetMidY(frame);
92 | self.hostLabel.alpha = MAX(height - yoffset, 0)/frame.size.height;
93 |
94 | self.secureIcon.alpha = self.hostLabel.alpha;
95 | self.secureIcon.center = CGPointMake(frame.origin.x - iconWidth*0.5 - 4, self.hostLabel.center.y);
96 |
97 | yoffset += margin;
98 | CGFloat bottom = self.tipsLabel.bounds.size.height + margin * 2;
99 | self.refreshIcon.center = CGPointMake(width * 0.5, (MIN(height, kRefreshMaxHeight-bottom) + yoffset) * 0.5);
100 | self.refreshIcon.alpha = MIN(MAX((height - yoffset)/kRefreshLogoSize - 1, 0), 1);
101 | CGFloat rate = (self.refreshIcon.center.y - yoffset) * 2 / (kRefreshMaxHeight-bottom-yoffset);
102 | self.refreshIcon.transform = CGAffineTransformMakeRotation((rate * 1.5 + 1) * M_PI);
103 |
104 | rate = MIN(MAX(height - (kRefreshMaxHeight-bottom), 0)/bottom, 1);
105 | CGFloat bgSize = rate * kRefreshButton;
106 | self.refreshIconBG.center = self.refreshIcon.center;
107 | self.refreshIconBG.bounds = CGRectMake(0, 0, bgSize, bgSize);
108 | self.refreshIconBG.layer.cornerRadius = bgSize * 0.5;
109 |
110 | frame = self.tipsLabel.frame;
111 | frame.size.width = width;
112 | frame.origin.y = self.refreshIconBG.center.y + kRefreshButton*0.5 + margin * 2;
113 | self.tipsLabel.frame = frame;
114 | self.tipsLabel.alpha = rate;
115 | }
116 |
117 | - (void)didMoveToSuperview {
118 | [super didMoveToSuperview];
119 | [self.scrollView.panGestureRecognizer addTarget:self action:@selector(actionPanGestureRecognizer:)];
120 | }
121 |
122 | - (void)removeFromSuperview {
123 | [self.scrollView.panGestureRecognizer removeTarget:self action:@selector(actionPanGestureRecognizer:)];
124 | [super removeFromSuperview];
125 | }
126 |
127 | - (void)sendActionsForControlEvents:(UIControlEvents)controlEvents {
128 | if (controlEvents == UIControlEventValueChanged) {
129 | _activeOffset = MAX(-self.scrollView.contentOffset.y, 0);
130 | [super endRefreshing]; // Note: Disable default value changed
131 | } else {
132 | [super sendActionsForControlEvents:controlEvents];
133 | }
134 | }
135 |
136 | - (void)beginRefreshing {
137 | // Do nothing
138 | }
139 |
140 | - (void)setHost:(NSString *)host {
141 | if (![_host isEqualToString:host]) {
142 | _host = host;
143 | self.hostLabel.text = host;
144 | [self setNeedsLayout];
145 | }
146 | }
147 |
148 | - (void)setHasOnlySecureContent:(BOOL)hasOnlySecureContent {
149 | if (_hasOnlySecureContent != hasOnlySecureContent) {
150 | AGTheme *theme = AGTheme.shared;
151 | _hasOnlySecureContent = hasOnlySecureContent;
152 | if (hasOnlySecureContent) {
153 | self.secureIcon.tintColor = theme.secureColor;
154 | self.secureIcon.image = [UIImage imageWithSymbol:@"lock.fill"];
155 | } else {
156 | self.secureIcon.tintColor = theme.warnColor;
157 | self.secureIcon.image = [UIImage imageWithSymbol:@"exclamationmark.triangle.fill"];
158 | }
159 | }
160 | }
161 |
162 | #pragma mark - Action Methods
163 | - (void)actionPanGestureRecognizer:(UIPanGestureRecognizer *)recognizer {
164 | if (recognizer.state == UIGestureRecognizerStateEnded && MAX(-self.scrollView.contentOffset.y, 0) >= self.activeOffset) {
165 | @weakify(self);
166 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.4 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
167 | @strongify(self);
168 | [self sendRefreshChanged];
169 | });
170 | }
171 | }
172 |
173 | #pragma mark - Private Methods
174 | - (UIScrollView *)scrollView {
175 | UIScrollView *scrollView = nil;
176 | if ([self.superview isKindOfClass:UIScrollView.class]) {
177 | scrollView = (UIScrollView *)self.superview;
178 | }
179 | return scrollView;
180 | }
181 |
182 | - (void)sendRefreshChanged {
183 | [super sendActionsForControlEvents:UIControlEventValueChanged];
184 | }
185 |
186 |
187 | @end
188 |
--------------------------------------------------------------------------------
/Argus/Extensions/NSData+AGExt.m:
--------------------------------------------------------------------------------
1 | //
2 | // NSData+AGExt.m
3 | // Argus
4 | //
5 | // Created by WizJin on 2020/11/30.
6 | //
7 |
8 | #import "NSData+AGExt.h"
9 | #import
10 | #import
11 | #import
12 |
13 | #define kCompressChunkSize 2048
14 | #define kCompressLevel Z_BEST_COMPRESSION
15 |
16 | static const char *tbl = "0123456789ABCDEF";
17 |
18 | @implementation NSData (AGExt)
19 |
20 | + (nullable instancetype)dataWithBase32EncodedString:(NSString *)base32String {
21 | size_t len = base32String.length;
22 | if (len > 0) {
23 | NSMutableData *data = [NSMutableData dataWithLength:(len*5 + 7)/8];
24 | size_t cnt = base32_decode((const uint8_t *)base32String.UTF8String, len, data.mutableBytes, data.length);
25 | if (cnt > 0) {
26 | data.length = cnt;
27 | return data;
28 | }
29 | }
30 | return nil;
31 | }
32 |
33 | - (NSString *)base32EncodedString {
34 | NSMutableData *data = [NSMutableData dataWithLength:((self.length + 4)/5)*8 + 1];
35 | int len = base32_encode(self.bytes, self.length, data.mutableBytes, data.length);
36 | data.length = (len <= 0 ? 0 : len);
37 | return [[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding];
38 | }
39 |
40 | - (NSData *)sha1 {
41 | NSMutableData *data = [NSMutableData dataWithLength:CC_SHA1_DIGEST_LENGTH];
42 | CC_SHA1(self.bytes, (CC_LONG)self.length, data.mutableBytes);
43 | return data;
44 | }
45 |
46 | - (NSString *)hex {
47 | NSString *res = @"";
48 | size_t len = self.length;
49 | if (len > 0) {
50 | const uint8_t *ptr = self.bytes;
51 | NSMutableData *data = [NSMutableData dataWithLength:sizeof(uint16_t)*len];
52 | uint16_t *pout = data.mutableBytes;
53 | if (pout != NULL) {
54 | for (int i = 0; i < len; i++) {
55 | uint8_t c = ptr[i];
56 | #if BYTE_ORDER == BIG_ENDIAN
57 | pout[i] = (uint16_t)(tbl[c&0x0f]) | ((uint16_t)(tbl[(c>>4)&0x0f]) << 8);
58 | #else
59 | pout[i] = ((uint16_t)(tbl[c&0x0f]) << 8) | (uint16_t)(tbl[(c>>4)&0x0f]);
60 | #endif
61 | }
62 | res = [[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding];;
63 | }
64 | }
65 | return res;
66 | }
67 |
68 | - (NSData *)compress {
69 | NSMutableData *outData = nil;
70 | if (self.length > 0) {
71 | z_stream zStream;
72 | bzero(&zStream, sizeof(zStream));
73 | zStream.zalloc = Z_NULL;
74 | zStream.zfree = Z_NULL;
75 | zStream.opaque = Z_NULL;
76 | zStream.next_in = (Bytef *)self.bytes;
77 | zStream.avail_in = (uint)self.length;
78 | zStream.total_out = 0;
79 | int status = deflateInit2(&zStream, kCompressLevel, Z_DEFLATED, -15, 8, Z_DEFAULT_STRATEGY);
80 | outData = [NSMutableData dataWithLength:kCompressChunkSize];
81 | do {
82 | if ((status == Z_BUF_ERROR) || (zStream.total_out == outData.length)) {
83 | outData.length += kCompressChunkSize;
84 | }
85 | zStream.next_out = (Bytef *)outData.mutableBytes + zStream.total_out;
86 | zStream.avail_out = (uInt)(outData.length - zStream.total_out);
87 | status = deflate(&zStream, Z_FINISH);
88 | } while ((status == Z_BUF_ERROR) || (status == Z_OK));
89 | status = deflateEnd(&zStream);
90 | if ((status == Z_OK) || (status == Z_STREAM_END)) {
91 | outData.length = zStream.total_out;
92 | } else {
93 | outData.length = 0;
94 | }
95 | }
96 | return outData != nil ? outData : [NSData new];
97 | }
98 |
99 | - (NSData *)decompress {
100 | NSMutableData *outData = nil;
101 | if (self.length > 0) {
102 | z_stream zStream;
103 | bzero(&zStream, sizeof(zStream));
104 | zStream.zalloc = Z_NULL;
105 | zStream.zfree = Z_NULL;
106 | zStream.opaque = Z_NULL;
107 | zStream.next_in = (Bytef *)self.bytes;
108 | zStream.avail_in = (uInt)self.length;
109 | zStream.total_out = 0;
110 | int status = inflateInit2(&zStream, -15);
111 | outData = [NSMutableData dataWithLength:kCompressChunkSize];
112 | do {
113 | if ((status == Z_BUF_ERROR) || (zStream.total_out == outData.length)) {
114 | outData.length += kCompressChunkSize;
115 | }
116 | zStream.next_out = (Bytef *)outData.mutableBytes + zStream.total_out;
117 | zStream.avail_out = (uInt)(outData.length - zStream.total_out);
118 | status = inflate(&zStream, Z_FINISH);
119 | } while ((status == Z_BUF_ERROR) || (status == Z_OK));
120 | status = inflateEnd(&zStream);
121 | if ((status != Z_OK) && (status != Z_STREAM_END)) {
122 | zStream.total_out = 0;
123 | }
124 | outData.length = zStream.total_out;
125 | }
126 | return outData != nil ? outData : [NSData new];
127 | }
128 |
129 | #pragma mark - Private Methods
130 | static inline int base32_decode(const uint8_t *ptr, size_t len, uint8_t *outbuf, size_t outsize) {
131 | int cnt = 0;
132 | int buffer = 0;
133 | int bitsLeft = 0;
134 | for (int i = 0; i < len; i++) {
135 | uint8_t ch = ptr[i];
136 | switch (ch) {
137 | case ' ': case '\t': case '\r': case '\n': case '-':
138 | continue;
139 | case 'A':case 'B':case 'C':case 'D':case 'E':case 'F':case 'G':case 'H':case 'I':case 'J':case 'K':case 'L':case 'M':
140 | case 'N':case 'O':case 'P':case 'Q':case 'R':case 'S':case 'T':case 'U':case 'V':case 'W':case 'X':case 'Y':case 'Z':
141 | ch -= 'A';
142 | break;
143 | case 'a':case 'b':case 'c':case 'd':case 'e':case 'f':case 'g':case 'h':case 'i':case 'j':case 'k':case 'l':case 'm':
144 | case 'n':case 'o':case 'p':case 'q':case 'r':case 's':case 't':case 'u':case 'v':case 'w':case 'x':case 'y':case 'z':
145 | ch -= 'a';
146 | break;
147 | case '2':case '3':case '4':case '5':case '6':case '7':
148 | ch -= '2' - 26;
149 | break;
150 | case '0': ch = 'O' - 'A'; break;
151 | case '1': ch = 'L' - 'A'; break;
152 | case '8': ch = 'B' - 'A'; break;
153 | default:
154 | return -1;
155 | }
156 | buffer <<= 5;
157 | buffer |= ch;
158 | bitsLeft += 5;
159 | if (bitsLeft >= 8) {
160 | outbuf[cnt++] = buffer >> (bitsLeft - 8);
161 | bitsLeft -= 8;
162 | if (cnt > outsize) {
163 | break;
164 | }
165 | }
166 | }
167 | return cnt;
168 | }
169 |
170 | static inline int base32_encode(const uint8_t *ptr, size_t len, uint8_t *outbuf, size_t outsize) {
171 | int count = -1;
172 | if (len >= 0 && len <= (1 << 28)) {
173 | count = 0;
174 | if (len > 0) {
175 | int buffer = ptr[0];
176 | int next = 1;
177 | int bitsLeft = 8;
178 | while (count < outsize && (bitsLeft > 0 || next < len)) {
179 | if (bitsLeft < 5) {
180 | if (next < len) {
181 | buffer <<= 8;
182 | buffer |= ptr[next++] & 0xFF;
183 | bitsLeft += 8;
184 | } else {
185 | int pad = 5 - bitsLeft;
186 | buffer <<= pad;
187 | bitsLeft += pad;
188 | }
189 | }
190 | int index = 0x1F & (buffer >> (bitsLeft - 5));
191 | bitsLeft -= 5;
192 | outbuf[count++] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"[index];
193 | }
194 | }
195 | if (count < outsize) {
196 | outbuf[count] = '\0';
197 | }
198 | }
199 | return count;
200 | }
201 |
202 |
203 | @end
204 |
--------------------------------------------------------------------------------