├── 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 | [![iTunes App Store](https://img.shields.io/itunes/v/1542617858?logo=apple&style=for-the-badge)](https://itunes.apple.com/app/id1542617858) 4 | [![GitHub](https://img.shields.io/github/license/wizjin/Argus?style=for-the-badge)](LICENSE) 5 | 6 | Argus - TOTP Authenticator Tools | Product Hunt 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 | ![Guide](guide.gif) 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 | --------------------------------------------------------------------------------