├── docs ├── CNAME ├── .ruby-version ├── .gitignore ├── 404.html ├── favicon.ico ├── image │ ├── imfull.png │ ├── main.png │ ├── hakumai.jpg │ ├── kome_toumi.png │ ├── taue_man2.png │ ├── food_kome_masu.png │ ├── job_kome_nouka.png │ └── nougyou_inekari.png ├── Gemfile ├── _includes │ ├── head.html │ └── scripts.html ├── _config.yml ├── README.md ├── _layouts │ └── default.html ├── Gemfile.lock ├── index.md └── css │ ├── syntax.css │ └── solo.css ├── .ruby-version ├── protobuf ├── nicolive-comment-protobuf │ ├── VERSION │ └── dwango │ │ └── nicolive │ │ └── chat │ │ ├── data │ │ ├── origin.proto │ │ ├── message.proto │ │ ├── state.proto │ │ └── atoms │ │ │ └── moderator.proto │ │ └── service │ │ └── edge │ │ └── payload.proto └── README.md ├── document ├── image │ └── logo.png └── screenshot │ └── main.png ├── Gemfile ├── Hakumai ├── Resources │ ├── Images.xcassets │ │ ├── Contents.json │ │ ├── Images │ │ │ ├── Contents.json │ │ │ ├── DefaultUserImage.imageset │ │ │ │ ├── blank.jpg │ │ │ │ └── Contents.json │ │ │ └── DefaultLiveThumbnailImage.imageset │ │ │ │ ├── DefaultCommunityImage.jpg │ │ │ │ └── Contents.json │ │ └── icons │ │ │ ├── Contents.json │ │ │ ├── Main │ │ │ ├── Contents.json │ │ │ ├── Statistics │ │ │ │ ├── Contents.json │ │ │ │ ├── gift_points.imageset │ │ │ │ │ ├── atm.png │ │ │ │ │ ├── atm@2x.png │ │ │ │ │ ├── atm@3x.png │ │ │ │ │ └── Contents.json │ │ │ │ ├── ad_points.imageset │ │ │ │ │ ├── ad_points.png │ │ │ │ │ ├── ad_points@2x.png │ │ │ │ │ ├── ad_points@3x.png │ │ │ │ │ └── Contents.json │ │ │ │ ├── comment_count.imageset │ │ │ │ │ ├── comment_count.png │ │ │ │ │ ├── comment_count@2x.png │ │ │ │ │ ├── comment_count@3x.png │ │ │ │ │ └── Contents.json │ │ │ │ └── visitor_count.imageset │ │ │ │ │ ├── visitor_count.png │ │ │ │ │ ├── visitor_count@2x.png │ │ │ │ │ ├── visitor_count@3x.png │ │ │ │ │ └── Contents.json │ │ │ ├── link_black.imageset │ │ │ │ ├── outline_link_black_48pt_1x.png │ │ │ │ ├── outline_link_black_48pt_2x.png │ │ │ │ ├── outline_link_black_48pt_3x.png │ │ │ │ └── Contents.json │ │ │ ├── stop_black.imageset │ │ │ │ ├── outline_stop_black_48pt_1x.png │ │ │ │ ├── outline_stop_black_48pt_2x.png │ │ │ │ ├── outline_stop_black_48pt_3x.png │ │ │ │ └── Contents.json │ │ │ ├── people_black.imageset │ │ │ │ ├── outline_people_black_72pt_1x.png │ │ │ │ ├── outline_people_black_72pt_2x.png │ │ │ │ ├── outline_people_black_72pt_3x.png │ │ │ │ └── Contents.json │ │ │ ├── schedule_black.imageset │ │ │ │ ├── outline_schedule_black_72pt_1x.png │ │ │ │ ├── outline_schedule_black_72pt_2x.png │ │ │ │ ├── outline_schedule_black_72pt_3x.png │ │ │ │ └── Contents.json │ │ │ ├── play_arrow_black.imageset │ │ │ │ ├── outline_play_arrow_black_48pt_1x.png │ │ │ │ ├── outline_play_arrow_black_48pt_2x.png │ │ │ │ ├── outline_play_arrow_black_48pt_3x.png │ │ │ │ └── Contents.json │ │ │ └── military_tech_black.imageset │ │ │ │ ├── outline_military_tech_black_48pt_1x.png │ │ │ │ ├── outline_military_tech_black_48pt_2x.png │ │ │ │ ├── outline_military_tech_black_48pt_3x.png │ │ │ │ └── Contents.json │ │ │ ├── Premium │ │ │ ├── Contents.json │ │ │ ├── PremiumMisc.imageset │ │ │ │ ├── misc_dark.png │ │ │ │ ├── misc_dark@2x.png │ │ │ │ ├── misc_dark@3x.png │ │ │ │ └── Contents.json │ │ │ ├── PremiumIppan.imageset │ │ │ │ ├── ippan_dark.png │ │ │ │ ├── ippan_dark@2x.png │ │ │ │ ├── ippan_dark@3x.png │ │ │ │ └── Contents.json │ │ │ └── PremiumPremium.imageset │ │ │ │ ├── premium_dark.png │ │ │ │ ├── premium_dark@2x.png │ │ │ │ ├── premium_dark@3x.png │ │ │ │ └── Contents.json │ │ │ ├── Scroll │ │ │ ├── Contents.json │ │ │ ├── arrow_upward_black.imageset │ │ │ │ ├── outline_arrow_upward_black_48pt_1x.png │ │ │ │ ├── outline_arrow_upward_black_48pt_2x.png │ │ │ │ ├── outline_arrow_upward_black_48pt_3x.png │ │ │ │ └── Contents.json │ │ │ └── arrow_downward_black.imageset │ │ │ │ ├── outline_arrow_downward_black_48pt_1x.png │ │ │ │ ├── outline_arrow_downward_black_48pt_2x.png │ │ │ │ ├── outline_arrow_downward_black_48pt_3x.png │ │ │ │ └── Contents.json │ │ │ └── UserId │ │ │ ├── Contents.json │ │ │ ├── UserId184Id.imageset │ │ │ ├── ippan_dark.png │ │ │ ├── ippan_dark@2x.png │ │ │ ├── ippan_dark@3x.png │ │ │ └── Contents.json │ │ │ ├── UserIdRawId.imageset │ │ │ ├── user_id_raw_dark.png │ │ │ ├── user_id_raw_dark@2x.png │ │ │ ├── user_id_raw_dark@3x.png │ │ │ └── Contents.json │ │ │ ├── HandleNameOver184Id.imageset │ │ │ ├── handle_over_184_dark.png │ │ │ ├── handle_over_184_dark@2x.png │ │ │ ├── handle_over_184_dark@3x.png │ │ │ └── Contents.json │ │ │ └── HandleNameOverRawId.imageset │ │ │ ├── handle_over_user_id_dark.png │ │ │ ├── handle_over_user_id_dark@2x.png │ │ │ ├── handle_over_user_id_dark@3x.png │ │ │ └── Contents.json │ └── AppIcon.icon │ │ ├── Assets │ │ └── icon_512x512@2x.png │ │ └── icon.json ├── Hakumai-Bridging-Header.h ├── Hakumai.entitlements ├── Controllers │ ├── MainWindowController │ │ ├── MainWindow.swift │ │ ├── TableCellView │ │ │ ├── ColoredView.swift │ │ │ ├── IconTableCellView.swift │ │ │ ├── PremiumTableCellView.swift │ │ │ ├── RoomPositionTableCellView.swift │ │ │ ├── CommentTableCellView.swift │ │ │ ├── IconTableCellView.xib │ │ │ ├── TimeTableCellView.swift │ │ │ ├── TimeTableCellView.xib │ │ │ ├── PremiumTableCellView.xib │ │ │ ├── UserIdTableCellView.xib │ │ │ ├── RoomPositionTableCellView.xib │ │ │ └── UserIdTableCellView.swift │ │ └── HandleNameAddViewController.swift │ ├── PreferenceWindowController │ │ ├── PreferenceWindow.swift │ │ ├── MuteAddViewController.swift │ │ └── MuteViewController.swift │ ├── CustomView │ │ ├── PointingHandButton.swift │ │ ├── AppearanceMonitorView.swift │ │ ├── CircleImageView.swift │ │ ├── ClickTableView.swift │ │ └── LiveThumbnailImageView.swift │ ├── UserWindowController │ │ ├── UserWindow.swift │ │ └── UserWindowController.swift │ ├── AuthWindowController │ │ ├── AuthWindowController.swift │ │ └── AuthViewController.swift │ ├── BrowserHelper.swift │ └── UIHelper.swift ├── Managers │ ├── CommentDetector │ │ ├── StoreCommentDetectorType.swift │ │ ├── KusaCommentDetectorType.swift │ │ └── StoreCommentDetector.swift │ ├── BrowserUrlObserver │ │ ├── IgnoreLiveRegistryType.swift │ │ ├── BrowserUrlObserverType.swift │ │ ├── IgnoreLiveRegistry.swift │ │ └── BrowserUrlObserver.swift │ ├── VoicevoxWrapper │ │ └── VoicevoxWrapperModel.swift │ ├── CommentCopier │ │ └── CommentCopierType.swift │ ├── LiveThumbnailManager │ │ └── LiveThumbnailManagerType.swift │ ├── RankingManager │ │ └── RankingManagerProtocol.swift │ ├── NicoManager │ │ ├── NdgrClientProtocol.swift │ │ └── NicoManagerProtocol.swift │ ├── HandleNameManager │ │ └── DatabaseValueCacher.swift │ ├── AuthManager │ │ └── TokenStore.swift │ ├── SpeechManager │ │ └── AudioCacher.swift │ └── MessageContainer │ │ └── Message.swift ├── Extensions │ ├── Int+Extensions.swift │ ├── AFError+Extensions.swift │ ├── NSWindow+Extensions.swift │ ├── L10n+Extensions.swift │ ├── NSView+Extensions.swift │ ├── NSColor+Extensions.swift │ └── NSScrollView+Extensions.swift ├── Models │ ├── Application │ │ ├── LiveStatistics.swift │ │ ├── User.swift │ │ ├── ProgramProvider.swift │ │ ├── Live.swift │ │ ├── Chat.swift │ │ └── Enum.swift │ ├── NicoManager │ │ ├── NicoRestApiModel.swift │ │ ├── NicoOAuthApiModel.swift │ │ └── NicoWebSocketModel.swift │ └── AuthManager │ │ └── AuthModel.swift ├── OAuthCredential.sample.swift ├── Global.swift ├── Info.plist ├── Parameter.swift └── Infrastructures │ └── Keychain │ └── KeychainUtility.swift ├── HakumaiTests ├── HakumaiTests-Bridging-Header.h ├── Resources │ ├── wsendpoint_response.json │ └── userinfo_response.json ├── HakumaiTests.swift ├── Models │ ├── RankingManagerTests.swift │ ├── PremiumTests.swift │ ├── ChatMessageTests.swift │ ├── DatabaseValueCacherTests.swift │ ├── NicoManagerTests.swift │ ├── LiveThumbnailManagerTest.swift │ └── HandleNameManagerTests.swift ├── Info.plist ├── TestUtilities │ └── StringTestExtension.swift └── Extensions │ └── StringExtensionTests.swift ├── Hakumai.xcodeproj └── project.xcworkspace │ └── contents.xcworkspacedata ├── .gitattributes ├── .repomixignore ├── .swiftlint.yml ├── swiftgen.yml ├── Podfile ├── .gitignore ├── README.md ├── LICENSE.md ├── .github └── workflows │ └── build-test.yml ├── Podfile.lock ├── AGENTS.md └── Gemfile.lock /docs/CNAME: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.7.3 2 | -------------------------------------------------------------------------------- /docs/.ruby-version: -------------------------------------------------------------------------------- 1 | 2.7.3 2 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | _site 2 | .jekyll-cache/ 3 | .idea/ 4 | -------------------------------------------------------------------------------- /protobuf/nicolive-comment-protobuf/VERSION: -------------------------------------------------------------------------------- 1 | v2024.0730.090426 -------------------------------------------------------------------------------- /docs/404.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | --- 4 | 5 | Page Not Found. -------------------------------------------------------------------------------- /docs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honishi/Hakumai/HEAD/docs/favicon.ico -------------------------------------------------------------------------------- /docs/image/imfull.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honishi/Hakumai/HEAD/docs/image/imfull.png -------------------------------------------------------------------------------- /docs/image/main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honishi/Hakumai/HEAD/docs/image/main.png -------------------------------------------------------------------------------- /docs/image/hakumai.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honishi/Hakumai/HEAD/docs/image/hakumai.jpg -------------------------------------------------------------------------------- /document/image/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honishi/Hakumai/HEAD/document/image/logo.png -------------------------------------------------------------------------------- /docs/image/kome_toumi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honishi/Hakumai/HEAD/docs/image/kome_toumi.png -------------------------------------------------------------------------------- /docs/image/taue_man2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honishi/Hakumai/HEAD/docs/image/taue_man2.png -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'cocoapods', '1.16.2' 4 | gem 'xcpretty', '0.3.0' 5 | -------------------------------------------------------------------------------- /document/screenshot/main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honishi/Hakumai/HEAD/document/screenshot/main.png -------------------------------------------------------------------------------- /docs/image/food_kome_masu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honishi/Hakumai/HEAD/docs/image/food_kome_masu.png -------------------------------------------------------------------------------- /docs/image/job_kome_nouka.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honishi/Hakumai/HEAD/docs/image/job_kome_nouka.png -------------------------------------------------------------------------------- /docs/image/nougyou_inekari.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honishi/Hakumai/HEAD/docs/image/nougyou_inekari.png -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/Images/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Hakumai/Hakumai-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/Main/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/Premium/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/Scroll/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/UserId/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/Main/Statistics/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Hakumai/Resources/AppIcon.icon/Assets/icon_512x512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honishi/Hakumai/HEAD/Hakumai/Resources/AppIcon.icon/Assets/icon_512x512@2x.png -------------------------------------------------------------------------------- /docs/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } 6 | 7 | gem "jekyll" 8 | -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/Images/DefaultUserImage.imageset/blank.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honishi/Hakumai/HEAD/Hakumai/Resources/Images.xcassets/Images/DefaultUserImage.imageset/blank.jpg -------------------------------------------------------------------------------- /HakumaiTests/HakumaiTests-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | 5 | #import "Hakumai-Bridging-Header.h" 6 | -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/Premium/PremiumMisc.imageset/misc_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honishi/Hakumai/HEAD/Hakumai/Resources/Images.xcassets/icons/Premium/PremiumMisc.imageset/misc_dark.png -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/UserId/UserId184Id.imageset/ippan_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honishi/Hakumai/HEAD/Hakumai/Resources/Images.xcassets/icons/UserId/UserId184Id.imageset/ippan_dark.png -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/Main/Statistics/gift_points.imageset/atm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honishi/Hakumai/HEAD/Hakumai/Resources/Images.xcassets/icons/Main/Statistics/gift_points.imageset/atm.png -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/Premium/PremiumIppan.imageset/ippan_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honishi/Hakumai/HEAD/Hakumai/Resources/Images.xcassets/icons/Premium/PremiumIppan.imageset/ippan_dark.png -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/Premium/PremiumMisc.imageset/misc_dark@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honishi/Hakumai/HEAD/Hakumai/Resources/Images.xcassets/icons/Premium/PremiumMisc.imageset/misc_dark@2x.png -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/Premium/PremiumMisc.imageset/misc_dark@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honishi/Hakumai/HEAD/Hakumai/Resources/Images.xcassets/icons/Premium/PremiumMisc.imageset/misc_dark@3x.png -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/UserId/UserId184Id.imageset/ippan_dark@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honishi/Hakumai/HEAD/Hakumai/Resources/Images.xcassets/icons/UserId/UserId184Id.imageset/ippan_dark@2x.png -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/UserId/UserId184Id.imageset/ippan_dark@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honishi/Hakumai/HEAD/Hakumai/Resources/Images.xcassets/icons/UserId/UserId184Id.imageset/ippan_dark@3x.png -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/Main/Statistics/ad_points.imageset/ad_points.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honishi/Hakumai/HEAD/Hakumai/Resources/Images.xcassets/icons/Main/Statistics/ad_points.imageset/ad_points.png -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/Main/Statistics/gift_points.imageset/atm@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honishi/Hakumai/HEAD/Hakumai/Resources/Images.xcassets/icons/Main/Statistics/gift_points.imageset/atm@2x.png -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/Main/Statistics/gift_points.imageset/atm@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honishi/Hakumai/HEAD/Hakumai/Resources/Images.xcassets/icons/Main/Statistics/gift_points.imageset/atm@3x.png -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/Premium/PremiumIppan.imageset/ippan_dark@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honishi/Hakumai/HEAD/Hakumai/Resources/Images.xcassets/icons/Premium/PremiumIppan.imageset/ippan_dark@2x.png -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/Premium/PremiumIppan.imageset/ippan_dark@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honishi/Hakumai/HEAD/Hakumai/Resources/Images.xcassets/icons/Premium/PremiumIppan.imageset/ippan_dark@3x.png -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/Premium/PremiumPremium.imageset/premium_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honishi/Hakumai/HEAD/Hakumai/Resources/Images.xcassets/icons/Premium/PremiumPremium.imageset/premium_dark.png -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/UserId/UserIdRawId.imageset/user_id_raw_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honishi/Hakumai/HEAD/Hakumai/Resources/Images.xcassets/icons/UserId/UserIdRawId.imageset/user_id_raw_dark.png -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/Main/Statistics/ad_points.imageset/ad_points@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honishi/Hakumai/HEAD/Hakumai/Resources/Images.xcassets/icons/Main/Statistics/ad_points.imageset/ad_points@2x.png -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/Main/Statistics/ad_points.imageset/ad_points@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honishi/Hakumai/HEAD/Hakumai/Resources/Images.xcassets/icons/Main/Statistics/ad_points.imageset/ad_points@3x.png -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/Premium/PremiumPremium.imageset/premium_dark@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honishi/Hakumai/HEAD/Hakumai/Resources/Images.xcassets/icons/Premium/PremiumPremium.imageset/premium_dark@2x.png -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/Premium/PremiumPremium.imageset/premium_dark@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honishi/Hakumai/HEAD/Hakumai/Resources/Images.xcassets/icons/Premium/PremiumPremium.imageset/premium_dark@3x.png -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/UserId/UserIdRawId.imageset/user_id_raw_dark@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honishi/Hakumai/HEAD/Hakumai/Resources/Images.xcassets/icons/UserId/UserIdRawId.imageset/user_id_raw_dark@2x.png -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/UserId/UserIdRawId.imageset/user_id_raw_dark@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honishi/Hakumai/HEAD/Hakumai/Resources/Images.xcassets/icons/UserId/UserIdRawId.imageset/user_id_raw_dark@3x.png -------------------------------------------------------------------------------- /Hakumai.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/Main/Statistics/comment_count.imageset/comment_count.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honishi/Hakumai/HEAD/Hakumai/Resources/Images.xcassets/icons/Main/Statistics/comment_count.imageset/comment_count.png -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/Main/Statistics/visitor_count.imageset/visitor_count.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honishi/Hakumai/HEAD/Hakumai/Resources/Images.xcassets/icons/Main/Statistics/visitor_count.imageset/visitor_count.png -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/Main/link_black.imageset/outline_link_black_48pt_1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honishi/Hakumai/HEAD/Hakumai/Resources/Images.xcassets/icons/Main/link_black.imageset/outline_link_black_48pt_1x.png -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/Main/link_black.imageset/outline_link_black_48pt_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honishi/Hakumai/HEAD/Hakumai/Resources/Images.xcassets/icons/Main/link_black.imageset/outline_link_black_48pt_2x.png -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/Main/link_black.imageset/outline_link_black_48pt_3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honishi/Hakumai/HEAD/Hakumai/Resources/Images.xcassets/icons/Main/link_black.imageset/outline_link_black_48pt_3x.png -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/Main/stop_black.imageset/outline_stop_black_48pt_1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honishi/Hakumai/HEAD/Hakumai/Resources/Images.xcassets/icons/Main/stop_black.imageset/outline_stop_black_48pt_1x.png -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/Main/stop_black.imageset/outline_stop_black_48pt_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honishi/Hakumai/HEAD/Hakumai/Resources/Images.xcassets/icons/Main/stop_black.imageset/outline_stop_black_48pt_2x.png -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/Main/stop_black.imageset/outline_stop_black_48pt_3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honishi/Hakumai/HEAD/Hakumai/Resources/Images.xcassets/icons/Main/stop_black.imageset/outline_stop_black_48pt_3x.png -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # to fix wrong repository language issue 2 | # see detail at https://github.com/github/linguist#my-repository-is-detected-as-the-wrong-language 3 | HakumaiTests/Resources/*.html linguist-vendored 4 | docs/* linguist-documentation 5 | -------------------------------------------------------------------------------- /protobuf/README.md: -------------------------------------------------------------------------------- 1 | ```shell 2 | # prepare protoc 3 | brew install swift-protobuf 4 | 5 | # generate *.pb.swift files 6 | protoc --proto_path=./nicolive-comment-protobuf --swift_out=../Hakumai/Models nicolive-comment-protobuf/**/*.proto 7 | ``` 8 | -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/Images/DefaultLiveThumbnailImage.imageset/DefaultCommunityImage.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honishi/Hakumai/HEAD/Hakumai/Resources/Images.xcassets/Images/DefaultLiveThumbnailImage.imageset/DefaultCommunityImage.jpg -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/Main/Statistics/comment_count.imageset/comment_count@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honishi/Hakumai/HEAD/Hakumai/Resources/Images.xcassets/icons/Main/Statistics/comment_count.imageset/comment_count@2x.png -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/Main/Statistics/comment_count.imageset/comment_count@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honishi/Hakumai/HEAD/Hakumai/Resources/Images.xcassets/icons/Main/Statistics/comment_count.imageset/comment_count@3x.png -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/Main/Statistics/visitor_count.imageset/visitor_count@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honishi/Hakumai/HEAD/Hakumai/Resources/Images.xcassets/icons/Main/Statistics/visitor_count.imageset/visitor_count@2x.png -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/Main/Statistics/visitor_count.imageset/visitor_count@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honishi/Hakumai/HEAD/Hakumai/Resources/Images.xcassets/icons/Main/Statistics/visitor_count.imageset/visitor_count@3x.png -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/Main/people_black.imageset/outline_people_black_72pt_1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honishi/Hakumai/HEAD/Hakumai/Resources/Images.xcassets/icons/Main/people_black.imageset/outline_people_black_72pt_1x.png -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/Main/people_black.imageset/outline_people_black_72pt_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honishi/Hakumai/HEAD/Hakumai/Resources/Images.xcassets/icons/Main/people_black.imageset/outline_people_black_72pt_2x.png -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/Main/people_black.imageset/outline_people_black_72pt_3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honishi/Hakumai/HEAD/Hakumai/Resources/Images.xcassets/icons/Main/people_black.imageset/outline_people_black_72pt_3x.png -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/UserId/HandleNameOver184Id.imageset/handle_over_184_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honishi/Hakumai/HEAD/Hakumai/Resources/Images.xcassets/icons/UserId/HandleNameOver184Id.imageset/handle_over_184_dark.png -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/Main/schedule_black.imageset/outline_schedule_black_72pt_1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honishi/Hakumai/HEAD/Hakumai/Resources/Images.xcassets/icons/Main/schedule_black.imageset/outline_schedule_black_72pt_1x.png -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/Main/schedule_black.imageset/outline_schedule_black_72pt_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honishi/Hakumai/HEAD/Hakumai/Resources/Images.xcassets/icons/Main/schedule_black.imageset/outline_schedule_black_72pt_2x.png -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/Main/schedule_black.imageset/outline_schedule_black_72pt_3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honishi/Hakumai/HEAD/Hakumai/Resources/Images.xcassets/icons/Main/schedule_black.imageset/outline_schedule_black_72pt_3x.png -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/UserId/HandleNameOver184Id.imageset/handle_over_184_dark@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honishi/Hakumai/HEAD/Hakumai/Resources/Images.xcassets/icons/UserId/HandleNameOver184Id.imageset/handle_over_184_dark@2x.png -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/UserId/HandleNameOver184Id.imageset/handle_over_184_dark@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honishi/Hakumai/HEAD/Hakumai/Resources/Images.xcassets/icons/UserId/HandleNameOver184Id.imageset/handle_over_184_dark@3x.png -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/UserId/HandleNameOverRawId.imageset/handle_over_user_id_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honishi/Hakumai/HEAD/Hakumai/Resources/Images.xcassets/icons/UserId/HandleNameOverRawId.imageset/handle_over_user_id_dark.png -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/Main/play_arrow_black.imageset/outline_play_arrow_black_48pt_1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honishi/Hakumai/HEAD/Hakumai/Resources/Images.xcassets/icons/Main/play_arrow_black.imageset/outline_play_arrow_black_48pt_1x.png -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/Main/play_arrow_black.imageset/outline_play_arrow_black_48pt_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honishi/Hakumai/HEAD/Hakumai/Resources/Images.xcassets/icons/Main/play_arrow_black.imageset/outline_play_arrow_black_48pt_2x.png -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/Main/play_arrow_black.imageset/outline_play_arrow_black_48pt_3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honishi/Hakumai/HEAD/Hakumai/Resources/Images.xcassets/icons/Main/play_arrow_black.imageset/outline_play_arrow_black_48pt_3x.png -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/UserId/HandleNameOverRawId.imageset/handle_over_user_id_dark@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honishi/Hakumai/HEAD/Hakumai/Resources/Images.xcassets/icons/UserId/HandleNameOverRawId.imageset/handle_over_user_id_dark@2x.png -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/UserId/HandleNameOverRawId.imageset/handle_over_user_id_dark@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honishi/Hakumai/HEAD/Hakumai/Resources/Images.xcassets/icons/UserId/HandleNameOverRawId.imageset/handle_over_user_id_dark@3x.png -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/Main/military_tech_black.imageset/outline_military_tech_black_48pt_1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honishi/Hakumai/HEAD/Hakumai/Resources/Images.xcassets/icons/Main/military_tech_black.imageset/outline_military_tech_black_48pt_1x.png -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/Main/military_tech_black.imageset/outline_military_tech_black_48pt_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honishi/Hakumai/HEAD/Hakumai/Resources/Images.xcassets/icons/Main/military_tech_black.imageset/outline_military_tech_black_48pt_2x.png -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/Main/military_tech_black.imageset/outline_military_tech_black_48pt_3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honishi/Hakumai/HEAD/Hakumai/Resources/Images.xcassets/icons/Main/military_tech_black.imageset/outline_military_tech_black_48pt_3x.png -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/Scroll/arrow_upward_black.imageset/outline_arrow_upward_black_48pt_1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honishi/Hakumai/HEAD/Hakumai/Resources/Images.xcassets/icons/Scroll/arrow_upward_black.imageset/outline_arrow_upward_black_48pt_1x.png -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/Scroll/arrow_upward_black.imageset/outline_arrow_upward_black_48pt_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honishi/Hakumai/HEAD/Hakumai/Resources/Images.xcassets/icons/Scroll/arrow_upward_black.imageset/outline_arrow_upward_black_48pt_2x.png -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/Scroll/arrow_upward_black.imageset/outline_arrow_upward_black_48pt_3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honishi/Hakumai/HEAD/Hakumai/Resources/Images.xcassets/icons/Scroll/arrow_upward_black.imageset/outline_arrow_upward_black_48pt_3x.png -------------------------------------------------------------------------------- /HakumaiTests/Resources/wsendpoint_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "meta": { 3 | "status": 200, 4 | "errorCode": "OK" 5 | }, 6 | "data": { 7 | "url": "wss://a.live2.nicovideo.jp/unama/wsapi/v2/watch/11111?audience_token=22222_33333_44444_abcdef" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/Scroll/arrow_downward_black.imageset/outline_arrow_downward_black_48pt_1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honishi/Hakumai/HEAD/Hakumai/Resources/Images.xcassets/icons/Scroll/arrow_downward_black.imageset/outline_arrow_downward_black_48pt_1x.png -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/Scroll/arrow_downward_black.imageset/outline_arrow_downward_black_48pt_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honishi/Hakumai/HEAD/Hakumai/Resources/Images.xcassets/icons/Scroll/arrow_downward_black.imageset/outline_arrow_downward_black_48pt_2x.png -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/Scroll/arrow_downward_black.imageset/outline_arrow_downward_black_48pt_3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honishi/Hakumai/HEAD/Hakumai/Resources/Images.xcassets/icons/Scroll/arrow_downward_black.imageset/outline_arrow_downward_black_48pt_3x.png -------------------------------------------------------------------------------- /protobuf/nicolive-comment-protobuf/dwango/nicolive/chat/data/origin.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package dwango.nicolive.chat.data; 4 | 5 | message NicoliveOrigin { 6 | message Chat { 7 | int64 live_id = 1; 8 | } 9 | 10 | oneof origin { 11 | Chat chat = 1; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Hakumai/Hakumai.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.automation.apple-events 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Hakumai/Controllers/MainWindowController/MainWindow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainWindow.swift 3 | // Hakumai 4 | // 5 | // Created by Hiroyuki Onishi on 2022/06/09. 6 | // Copyright © 2022 Hiroyuki Onishi. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import AppKit 11 | 12 | final class MainWindow: NSWindow {} 13 | -------------------------------------------------------------------------------- /.repomixignore: -------------------------------------------------------------------------------- 1 | docs 2 | document 3 | Gemfile 4 | Gemfile.lock 5 | Hakumai/OAuthCredential.swift 6 | Hakumai/Generated 7 | Hakumai/Resources 8 | Hakumai/Storyboards 9 | Hakumai.xcodeproj 10 | Hakumai.xcworkspace 11 | HakumaiTests 12 | LICENSE.md 13 | Podfile.lock 14 | Pods 15 | protobuf 16 | script 17 | swiftgen.yml 18 | vendor 19 | -------------------------------------------------------------------------------- /HakumaiTests/Resources/userinfo_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "sub": "12345", 3 | "nickname": "test test", 4 | "profile": "https://www.nicovideo.jp/user/12345", 5 | "picture": "https://secure-dcdn.cdn.nimg.jp/nicoaccount/usericon/defaults/blank.jpg", 6 | "gender": "male", 7 | "zoneinfo": "Asia/Tokyo", 8 | "updated_at": 1622960415 9 | } 10 | -------------------------------------------------------------------------------- /docs/_includes/head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Hakumai/Managers/CommentDetector/StoreCommentDetectorType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StoreCommentDetectorType.swift 3 | // Hakumai 4 | // 5 | // Created by Hiroyuki Onishi on 2022/06/13. 6 | // Copyright © 2022 Hiroyuki Onishi. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol StoreCommentDetectorType { 12 | func isStoreComment(chat: Chat) -> Bool 13 | } 14 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | # Site settings 2 | title: Hakumai - Mac用ニコ生コメントビューア(コメビュ) 3 | tag_text: Hakumai 4 | description: Hakumaiは Mac 用のニコニコ生放送用のコメントビューア(コメビュ)です。 5 | canonical_url: https://honishi.github.io/Hakumai/ 6 | 7 | # Build settings 8 | markdown: kramdown 9 | 10 | exclude: ["README.md"] 11 | 12 | # Binary version 13 | binary_version: 3.8.3 14 | binary_date: 2025/9/17 15 | -------------------------------------------------------------------------------- /Hakumai/Extensions/Int+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IntExtension.swift 3 | // Hakumai 4 | // 5 | // Created by Hiroyuki Onishi on 12/8/14. 6 | // Copyright (c) 2014 Hiroyuki Onishi. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Int { 12 | func toDateAsTimeIntervalSince1970() -> Date { 13 | return Date(timeIntervalSince1970: Double(self)) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Hakumai/Models/Application/LiveStatistics.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Heatbeat.swift 3 | // Hakumai 4 | // 5 | // Created by Hiroyuki Onishi on 12/8/14. 6 | // Copyright (c) 2014 Hiroyuki Onishi. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct LiveStatistics { 12 | // MARK: - Properties 13 | let viewers: Int 14 | let comments: Int 15 | let adPoints: Int? 16 | let giftPoints: Int? 17 | } 18 | -------------------------------------------------------------------------------- /Hakumai/Managers/BrowserUrlObserver/IgnoreLiveRegistryType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IgnoreUrlRegistryType.swift 3 | // Hakumai 4 | // 5 | // Created by Hiroyuki Onishi on 2022/06/10. 6 | // Copyright © 2022 Hiroyuki Onishi. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol IgnoreLiveRegistryType { 12 | func add(liveProgramId: String, seconds: TimeInterval) 13 | func shouldIgnore(liveProgramId: String) -> Bool 14 | } 15 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Solo 2 | 3 | [Solo](http://solo.chibi.io) is a Jekyll theme that supports **single-page websites** only, but supports them well. Yes, it's responsive. 4 | 5 | ### [Demo & Documentation →](http://solo.chibi.io) 6 | 7 | # Preview contents locally 8 | 9 | ## setup 10 | 11 | ```sh 12 | rbenv install 13 | bundle install 14 | ``` 15 | 16 | ## run 17 | 18 | ```sh 19 | bundle exec jekyll serve 20 | # http://127.0.0.1:4000 21 | ``` 22 | -------------------------------------------------------------------------------- /Hakumai/Models/NicoManager/NicoRestApiModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NicoRestApiModel.swift 3 | // Hakumai 4 | // 5 | // Created by Hiroyuki Onishi on 2021/05/05. 6 | // Copyright © 2021 Hiroyuki Onishi. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: - Rest API 12 | struct UserNickname: Codable { 13 | struct Data: Codable { 14 | let id: String 15 | let nickname: String 16 | } 17 | 18 | let data: Data 19 | } 20 | -------------------------------------------------------------------------------- /HakumaiTests/HakumaiTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HakumaiTests.swift 3 | // HakumaiTests 4 | // 5 | // Created by Hiroyuki Onishi on 11/9/14. 6 | // Copyright (c) 2014 Hiroyuki Onishi. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import XCTest 11 | 12 | final class HakumaiTests: XCTestCase { 13 | override func setUp() { 14 | super.setUp() 15 | } 16 | 17 | override func tearDown() { 18 | super.tearDown() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /docs/_includes/scripts.html: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/Images/DefaultUserImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "blank.jpg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - large_tuple 3 | - type_name 4 | - todo 5 | - identifier_name 6 | - unneeded_override 7 | 8 | opt_in_rules: 9 | - force_unwrapping 10 | 11 | excluded: 12 | - fastlane 13 | - script 14 | - vendor 15 | - Pods 16 | - Hakumai.xcodeproj 17 | - Hakumai.xcworkspace 18 | - Hakumai/Generated 19 | - Hakumai/Models/dwango 20 | 21 | line_length: 22 | warning: 400 23 | error: 500 24 | file_length: 25 | warning: 800 26 | error: 1000 27 | -------------------------------------------------------------------------------- /Hakumai/Models/Application/User.swift: -------------------------------------------------------------------------------- 1 | // 2 | // User.swift 3 | // Hakumai 4 | // 5 | // Created by Hiroyuki Onishi on 11/23/14. 6 | // Copyright (c) 2014 Hiroyuki Onishi. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct User { 12 | // MARK: - Properties 13 | let userId: String 14 | let nickname: String 15 | } 16 | 17 | extension User: CustomStringConvertible { 18 | var description: String { "User: userId[\(userId)] nickname[\(nickname)]" } 19 | } 20 | -------------------------------------------------------------------------------- /Hakumai/Managers/VoicevoxWrapper/VoicevoxWrapperModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VoicevoxWrapperModel.swift 3 | // Hakumai 4 | // 5 | // Created by Hiroyuki Onishi on 2022/02/07. 6 | // Copyright © 2022 Hiroyuki Onishi. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct VoicevoxSpeakerResponse: Codable { 12 | let name: String 13 | let styles: [VoicevoxSpeakerStyle] 14 | } 15 | 16 | struct VoicevoxSpeakerStyle: Codable { 17 | let id: Int 18 | let name: String 19 | } 20 | -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/Images/DefaultLiveThumbnailImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "DefaultCommunityImage.jpg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Hakumai/Managers/CommentCopier/CommentCopierType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CommentCopierType.swift 3 | // Hakumai 4 | // 5 | // Created by Hiroyuki Onishi on 2022/06/18. 6 | // Copyright © 2022 Hiroyuki Onishi. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol CommentCopierType { 12 | static func make(live: Live, messageContainer: MessageContainer, nicoManager: NicoManagerType, handleNameManager: HandleNameManager) -> CommentCopierType 13 | func copy(completion: (() -> Void)?) 14 | } 15 | -------------------------------------------------------------------------------- /swiftgen.yml: -------------------------------------------------------------------------------- 1 | input_dir: Hakumai/ 2 | output_dir: Hakumai/Generated/ 3 | ib: 4 | inputs: 5 | - Storyboards 6 | outputs: 7 | templateName: scenes-swift5 8 | output: StoryBoards.swift 9 | xcassets: 10 | inputs: 11 | - Resources/Images.xcassets 12 | outputs: 13 | templateName: swift5 14 | output: Images.swift 15 | strings: 16 | inputs: 17 | - Resources/Localization/en.lproj/Localizable.strings 18 | outputs: 19 | templateName: structured-swift5 20 | output: Localize.swift 21 | -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/Premium/PremiumMisc.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "misc_dark.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "misc_dark@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "misc_dark@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/Premium/PremiumIppan.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "ippan_dark.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "ippan_dark@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "ippan_dark@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/UserId/UserId184Id.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "ippan_dark.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "ippan_dark@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "ippan_dark@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/Premium/PremiumPremium.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "premium_dark.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "premium_dark@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "premium_dark@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/UserId/UserIdRawId.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "user_id_raw_dark.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "user_id_raw_dark@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "user_id_raw_dark@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/UserId/HandleNameOver184Id.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "handle_over_184_dark.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "handle_over_184_dark@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "handle_over_184_dark@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/UserId/HandleNameOverRawId.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "handle_over_user_id_dark.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "handle_over_user_id_dark@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "handle_over_user_id_dark@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Hakumai/Managers/BrowserUrlObserver/BrowserUrlObserverType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BrowserUrlObserverType.swift 3 | // Hakumai 4 | // 5 | // Created by Hiroyuki Onishi on 2021/12/07. 6 | // Copyright © 2021 Hiroyuki Onishi. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol BrowserUrlObserverType { 12 | func setBrowserType(_ browser: BrowserInUseType) 13 | func start(delegate: BrowserUrlObserverDelegate) 14 | func stop() 15 | } 16 | 17 | protocol BrowserUrlObserverDelegate: AnyObject { 18 | func browserUrlObserver(_ browserUrlObserver: BrowserUrlObserverType, didGetUrl liveUrl: URL) 19 | } 20 | -------------------------------------------------------------------------------- /Hakumai/Models/AuthManager/AuthModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthModel.swift 3 | // Hakumai 4 | // 5 | // Created by Hiroyuki Onishi on 2021/05/21. 6 | // Copyright © 2021 Hiroyuki Onishi. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct TokenResponse: Codable { 12 | let accessToken: String 13 | let tokenType: String 14 | let expiresIn: Int 15 | let scope: String 16 | let refreshToken: String 17 | // `idToken` is provided when `grant_type` = `authorization_code`, but 18 | // is not provided when it's `refresh_token`. So making this optional. 19 | let idToken: String? 20 | } 21 | -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/Main/Statistics/gift_points.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "atm.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "atm@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "atm@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | }, 23 | "properties" : { 24 | "template-rendering-intent" : "template" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/Main/Statistics/ad_points.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "ad_points.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "ad_points@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "ad_points@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | }, 23 | "properties" : { 24 | "template-rendering-intent" : "template" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Hakumai/Managers/LiveThumbnailManager/LiveThumbnailManagerType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LiveThumbnailManagerProtocol.swift 3 | // Hakumai 4 | // 5 | // Created by Hiroyuki Onishi on 2021/12/04. 6 | // Copyright © 2021 Hiroyuki Onishi. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol LiveThumbnailManagerType { 12 | func start(for liveProgramId: String, delegate: LiveThumbnailManagerDelegate) 13 | func stop() 14 | } 15 | 16 | protocol LiveThumbnailManagerDelegate: AnyObject { 17 | func liveThumbnailManager(_ liveThumbnailManager: LiveThumbnailManagerType, didGetThumbnailUrl thumbnailUrl: URL, forLiveProgramId liveProgramId: String) 18 | } 19 | -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/Main/Statistics/comment_count.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "comment_count.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "comment_count@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "comment_count@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | }, 23 | "properties" : { 24 | "template-rendering-intent" : "template" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/Main/Statistics/visitor_count.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "visitor_count.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "visitor_count@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "visitor_count@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | }, 23 | "properties" : { 24 | "template-rendering-intent" : "template" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/Main/link_black.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "outline_link_black_48pt_1x.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "outline_link_black_48pt_2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "outline_link_black_48pt_3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | }, 23 | "properties" : { 24 | "template-rendering-intent" : "template" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/Main/stop_black.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "outline_stop_black_48pt_1x.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "outline_stop_black_48pt_2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "outline_stop_black_48pt_3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | }, 23 | "properties" : { 24 | "template-rendering-intent" : "template" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Hakumai/OAuthCredential.sample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OAuthCredential.sample.swift 3 | // Hakumai 4 | // 5 | // Created by Hiroyuki Onishi on 2021/07/16. 6 | // Copyright © 2021 Hiroyuki Onishi. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | let hakumaiClientId = "XXXXX" 12 | let hakumaiServerApiBaseUrl = "https://api.host" 13 | let hakumaiServerApiPathRefreshToken = "/path/to/refresh-token" 14 | let devAuthCallbackUrl = "https://dev.api.host/oauth-callback" 15 | let devHakumaiServerApiBaseUrl = "https://dev.api.host" 16 | let useDevServer = false 17 | 18 | // swift/lint:disable line_length 19 | let debugExpiredAccessToken = "XXXXX" 20 | // swift/lint:enable line_length 21 | -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/Main/people_black.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "outline_people_black_72pt_1x.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "outline_people_black_72pt_2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "outline_people_black_72pt_3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | }, 23 | "properties" : { 24 | "template-rendering-intent" : "template" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Hakumai/Managers/CommentDetector/KusaCommentDetectorType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KusaCommentDetectorType.swift 3 | // Hakumai 4 | // 5 | // Created by Hiroyuki Onishi on 2022/06/11. 6 | // Copyright © 2022 Hiroyuki Onishi. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol KusaCommentDetectorType { 12 | func start(delegate: KusaCommentDetectorDelegate) 13 | func stop() 14 | func add(chat: Chat) 15 | } 16 | 17 | protocol KusaCommentDetectorDelegate: AnyObject { 18 | func kusaCommentDetectorDidDetectKusa(_ kusaCommentDetector: KusaCommentDetectorType) 19 | func kusaCommentDetector(_ kusaCommentDetector: KusaCommentDetectorType, hasDebugMessage message: String) 20 | } 21 | -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/Main/schedule_black.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "outline_schedule_black_72pt_1x.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "outline_schedule_black_72pt_2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "outline_schedule_black_72pt_3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | }, 23 | "properties" : { 24 | "template-rendering-intent" : "template" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/Main/play_arrow_black.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "outline_play_arrow_black_48pt_1x.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "outline_play_arrow_black_48pt_2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "outline_play_arrow_black_48pt_3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | }, 23 | "properties" : { 24 | "template-rendering-intent" : "template" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/Scroll/arrow_upward_black.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "outline_arrow_upward_black_48pt_1x.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "outline_arrow_upward_black_48pt_2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "outline_arrow_upward_black_48pt_3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | }, 23 | "properties" : { 24 | "template-rendering-intent" : "template" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/Main/military_tech_black.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "outline_military_tech_black_48pt_1x.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "outline_military_tech_black_48pt_2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "outline_military_tech_black_48pt_3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | }, 23 | "properties" : { 24 | "template-rendering-intent" : "template" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Hakumai/Resources/Images.xcassets/icons/Scroll/arrow_downward_black.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "outline_arrow_downward_black_48pt_1x.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "outline_arrow_downward_black_48pt_2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "outline_arrow_downward_black_48pt_3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | }, 23 | "properties" : { 24 | "template-rendering-intent" : "template" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Hakumai/Controllers/PreferenceWindowController/PreferenceWindow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PreferenceWindow.swift 3 | // Hakumai 4 | // 5 | // Created by Hiroyuki Onishi on 12/15/14. 6 | // Copyright (c) 2014 Hiroyuki Onishi. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import AppKit 11 | 12 | final class PreferenceWindow: NSWindow { 13 | // MARK: - NSObject Overrides 14 | override func awakeFromNib() { 15 | super.awakeFromNib() 16 | // alwaysOnTop = true 17 | } 18 | 19 | // MARK: - NSResponder Overrides 20 | // http://genjiapp.com/blog/2012/10/25/how-to-develop-a-preferences-window-for-os-x-app.html 21 | override func cancelOperation(_ sender: Any?) { 22 | close() 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /HakumaiTests/Models/RankingManagerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RankingManagerTests.swift 3 | // HakumaiTests 4 | // 5 | // Created by Hiroyuki Onishi on 2021/11/02. 6 | // Copyright © 2021 Hiroyuki Onishi. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Hakumai 11 | 12 | class RankingManagerTests: XCTestCase { 13 | override func setUpWithError() throws {} 14 | override func tearDownWithError() throws {} 15 | 16 | func testExtractChikuranHtml() throws { 17 | let manager = RankingManager() 18 | let html = "chikuran.html".resourceFileToString() 19 | 20 | let result = manager.exposedExtractRankMap(from: html) 21 | // print(result) 22 | XCTAssert(!result.isEmpty) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Hakumai/Controllers/CustomView/PointingHandButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PointingHandButton.swift 3 | // Hakumai 4 | // 5 | // Created by Hiroyuki Onishi on 2022/02/19. 6 | // Copyright © 2022 Hiroyuki Onishi. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import AppKit 11 | 12 | final class PointingHandButton: NSButton { 13 | override func awakeFromNib() { 14 | configure() 15 | } 16 | 17 | override func resetCursorRects() { 18 | super.resetCursorRects() 19 | addCursorRect(bounds, cursor: .pointingHand) 20 | } 21 | } 22 | 23 | private extension PointingHandButton { 24 | func configure() { 25 | bezelStyle = .texturedRounded 26 | isBordered = false 27 | title = "" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Hakumai/Controllers/UserWindowController/UserWindow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserWindow.swift 3 | // Hakumai 4 | // 5 | // Created by Hiroyuki Onishi on 12/22/14. 6 | // Copyright (c) 2014 Hiroyuki Onishi. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import AppKit 11 | 12 | final class UserWindow: NSWindow { 13 | /* 14 | // MARK: - Properties 15 | // MARK: - Object Lifecycle 16 | 17 | // MARK: - NSResponder Overrides 18 | // http://genjiapp.com/blog/2012/10/25/how-to-develop-a-preferences-window-for-os-x-app.html 19 | override func cancelOperation(sender: AnyObject?) { 20 | close() 21 | } 22 | 23 | // MARK: - [Protocol] Functions 24 | // MARK: - Public Functions 25 | // MARK: - Internal Functions 26 | */ 27 | } 28 | -------------------------------------------------------------------------------- /Hakumai/Models/Application/ProgramProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProgramProvider.swift 3 | // Hakumai 4 | // 5 | // Created by Hiroyuki Onishi on 2024/08/11. 6 | // Copyright © 2024 Hiroyuki Onishi. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct ProgramProvider { 12 | // MARK: - Properties 13 | let programProviderId: String 14 | let name: String 15 | let profileUrl: URL 16 | let icons: Icons 17 | 18 | struct Icons { 19 | let uri150x150: URL 20 | let uri50x50: URL 21 | } 22 | } 23 | 24 | extension ProgramProvider: CustomStringConvertible { 25 | var description: String { 26 | "ProgramProvider: programProviderId[\(programProviderId)] name[\(name)] " + 27 | "profileUrl[\(profileUrl)]" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /protobuf/nicolive-comment-protobuf/dwango/nicolive/chat/data/message.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package dwango.nicolive.chat.data; 4 | import "dwango/nicolive/chat/data/atoms.proto"; 5 | import "dwango/nicolive/chat/data/atoms/moderator.proto"; 6 | 7 | 8 | 9 | message NicoliveMessage { 10 | 11 | oneof data { 12 | Chat chat = 1; 13 | 14 | SimpleNotification simple_notification = 7; 15 | Gift gift = 8; 16 | Nicoad nicoad = 9; 17 | 18 | GameUpdate game_update = 13; 19 | 20 | 21 | TagUpdated tag_updated = 17; 22 | 23 | 24 | atoms.ModeratorUpdated moderator_updated = 18; 25 | 26 | atoms.SSNGUpdated ssng_updated = 19; 27 | 28 | 29 | Chat overflowed_chat = 20; 30 | 31 | 32 | } 33 | 34 | reserved 2 to 6, 10 to 12, 14 to 16; 35 | } 36 | -------------------------------------------------------------------------------- /protobuf/nicolive-comment-protobuf/dwango/nicolive/chat/data/state.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package dwango.nicolive.chat.data; 4 | 5 | import "dwango/nicolive/chat/data/atoms.proto"; 6 | import "dwango/nicolive/chat/data/atoms/moderator.proto"; 7 | 8 | message NicoliveState { 9 | 10 | 11 | optional Statistics statistics = 1; 12 | 13 | 14 | optional Enquete enquete = 2; 15 | 16 | 17 | optional MoveOrder move_order = 3; 18 | 19 | 20 | optional Marquee marquee = 4; 21 | 22 | 23 | optional CommentLock comment_lock = 5; 24 | 25 | 26 | optional CommentMode comment_mode = 6; 27 | 28 | 29 | optional TrialPanel trial_panel = 7; 30 | 31 | 32 | optional ProgramStatus program_status = 9; 33 | 34 | 35 | optional atoms.ModerationAnnouncement moderation_announcement = 10; 36 | } 37 | -------------------------------------------------------------------------------- /HakumaiTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /HakumaiTests/Models/PremiumTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PremiumTests.swift 3 | // HakumaiTests 4 | // 5 | // Created by Hiroyuki Onishi on 2021/11/13. 6 | // Copyright © 2021 Hiroyuki Onishi. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Hakumai 11 | 12 | class PremiumTests: XCTestCase { 13 | override func setUpWithError() throws {} 14 | override func tearDownWithError() throws {} 15 | 16 | func testIsSystem() { 17 | XCTAssert(Premium.ippan.isSystem == false, "") 18 | XCTAssert(Premium.premium.isSystem == false, "") 19 | XCTAssert(Premium.system.isSystem == true, "") 20 | } 21 | 22 | func testIsUser() { 23 | XCTAssert(Premium.ippan.isUser == true, "") 24 | XCTAssert(Premium.premium.isUser == true, "") 25 | XCTAssert(Premium.system.isUser == false, "") 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /HakumaiTests/TestUtilities/StringTestExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StringTestExtension.swift 3 | // HakumaiTests 4 | // 5 | // Created by Hiroyuki Onishi on 2021/11/02. 6 | // Copyright © 2021 Hiroyuki Onishi. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // swiftlint:disable force_unwrapping 12 | extension String { 13 | func resourceFileToData() -> Data { 14 | let bundle = Bundle(for: NicoManagerTests.self) 15 | let path = bundle.path(forResource: self, ofType: nil) 16 | let fileHandle = FileHandle(forReadingAtPath: path!) 17 | let data = fileHandle?.readDataToEndOfFile() 18 | return data! 19 | } 20 | 21 | func resourceFileToString() -> String { 22 | let data = resourceFileToData() 23 | return String(decoding: data, as: UTF8.self) 24 | } 25 | } 26 | // swiftlint:enable force_unwrapping 27 | -------------------------------------------------------------------------------- /docs/_layouts/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {{ site.title }} 9 | 10 | 11 | 12 | 13 | 14 | {% include head.html %} 15 | 16 | 17 |
18 |

{{ site.tag_text }}

19 | {{ content }} 20 |
21 | {% include scripts.html %} 22 | 23 | 24 | -------------------------------------------------------------------------------- /Hakumai/Resources/AppIcon.icon/icon.json: -------------------------------------------------------------------------------- 1 | { 2 | "fill" : { 3 | "automatic-gradient" : "extended-srgb:0.00000,0.53333,1.00000,1.00000" 4 | }, 5 | "groups" : [ 6 | { 7 | "layers" : [ 8 | { 9 | "glass" : false, 10 | "image-name" : "icon_512x512@2x.png", 11 | "name" : "icon_512x512@2x", 12 | "position" : { 13 | "scale" : 1.25, 14 | "translation-in-points" : [ 15 | 0, 16 | 0 17 | ] 18 | } 19 | } 20 | ], 21 | "shadow" : { 22 | "kind" : "neutral", 23 | "opacity" : 0.5 24 | }, 25 | "translucency" : { 26 | "enabled" : true, 27 | "value" : 0.5 28 | } 29 | } 30 | ], 31 | "supported-platforms" : { 32 | "circles" : [ 33 | "watchOS" 34 | ], 35 | "squares" : "shared" 36 | } 37 | } -------------------------------------------------------------------------------- /Hakumai/Managers/RankingManager/RankingManagerProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RankingManagerProtocol.swift 3 | // Hakumai 4 | // 5 | // Created by Hiroyuki Onishi on 2021/10/30. 6 | // Copyright © 2021 Hiroyuki Onishi. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol RankingManagerType { 12 | func addDelegate(_ delegate: RankingManagerDelegate, for liveId: String) 13 | func removeDelegate(_ delegate: RankingManagerDelegate) 14 | var isRunning: Bool { get } 15 | } 16 | 17 | protocol RankingManagerDelegate: AnyObject { 18 | func rankingManager(_ rankingManager: RankingManagerType, didUpdateRank rank: Int?, for liveId: String, at date: Date?) 19 | func rankingManager(_ rankingManager: RankingManagerType, hasDebugMessage message: String) 20 | } 21 | 22 | extension RankingManagerDelegate { 23 | func rankingManager(_ rankingManager: RankingManagerType, hasDebugMessage message: String) {} 24 | } 25 | -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | platform :osx, '10.15' 2 | inhibit_all_warnings! 3 | 4 | target 'Hakumai' do 5 | use_frameworks! 6 | 7 | # Project Infrastructure 8 | pod 'Sparkle', '~> 2.1.0' 9 | pod 'SwiftLint', '~> 0.55.1' 10 | pod 'SwiftGen', '~> 6.5.1' 11 | pod 'SwiftProtobuf', '~> 1.0' 12 | pod 'XCGLogger', '~> 7.0.1' 13 | 14 | # Infrastructure 15 | pod 'Alamofire', '~> 5.10.2' 16 | pod 'FMDB', '~> 2.7.5' 17 | pod 'Kanna', '~> 5.2.7' 18 | pod 'SAMKeychain', '~> 1.5.3' 19 | pod 'Starscream', '~> 4.0.6' 20 | 21 | # User Interface 22 | pod 'DGCharts', '~> 5.0.0' 23 | pod 'Kingfisher', '~> 6.3.1' 24 | pod 'SnapKit', '~> 5.6.0' 25 | 26 | target 'HakumaiTests' do 27 | inherit! :search_paths 28 | end 29 | end 30 | 31 | post_install do |installer| 32 | installer.pods_project.targets.each do |target| 33 | target.build_configurations.each do |config| 34 | config.build_settings['MACOSX_DEPLOYMENT_TARGET'] = '10.15' 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io 2 | 3 | ### OS X ### 4 | .DS_Store 5 | 6 | ### IntelliJ ### 7 | .idea 8 | 9 | ### VSCode ### 10 | .vscode 11 | 12 | ### Bundler ### 13 | vendor/ 14 | .bundle 15 | 16 | ### Swift ### 17 | # Xcode 18 | # 19 | build/ 20 | *.pbxuser 21 | !default.pbxuser 22 | *.mode1v3 23 | !default.mode1v3 24 | *.mode2v3 25 | !default.mode2v3 26 | *.perspectivev3 27 | !default.perspectivev3 28 | *.xcworkspace 29 | !default.xcworkspace 30 | xcuserdata 31 | *.xccheckout 32 | *.moved-aside 33 | DerivedData 34 | *.hmap 35 | *.ipa 36 | *.xcuserstate 37 | *.xcscmblueprint 38 | 39 | # CocoaPods 40 | # 41 | # We recommend against adding the Pods directory to your .gitignore. However 42 | # you should judge for yourself, the pros and cons are mentioned at: 43 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control 44 | # 45 | Pods/ 46 | 47 | # Credential 48 | OAuthCredential.swift 49 | 50 | # Claude 51 | .claude/*.local.json 52 | -------------------------------------------------------------------------------- /Hakumai/Managers/CommentDetector/StoreCommentDetector.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StoreCommentDetector.swift 3 | // Hakumai 4 | // 5 | // Created by Hiroyuki Onishi on 2022/06/13. 6 | // Copyright © 2022 Hiroyuki Onishi. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | private let ignoreSecondsSinceLastDetection: TimeInterval = 3 12 | 13 | final class StoreCommentDetector { 14 | private var lastDetectionDate = Date() 15 | } 16 | 17 | extension StoreCommentDetector: StoreCommentDetectorType { 18 | func isStoreComment(chat: Chat) -> Bool { 19 | let now = Date() 20 | let elapsedEnoughTime = now.timeIntervalSince(lastDetectionDate) > ignoreSecondsSinceLastDetection 21 | // log.debug("\(elapsedEnoughTime), \(now.timeIntervalSince(lastDetectionDate))") 22 | guard elapsedEnoughTime else { return false } 23 | if chat.roomPosition == .arena { 24 | return false 25 | } 26 | lastDetectionDate = now 27 | return true 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Hakumai/Managers/NicoManager/NdgrClientProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NdgrClientProtocol.swift 3 | // Hakumai 4 | // 5 | // Created by Hiroyuki Onishi on 2024/08/03. 6 | // Copyright © 2024 Hiroyuki Onishi. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol NdgrClientType: AnyObject { 12 | // Properties 13 | var delegate: NdgrClientDelegate? { get set } 14 | 15 | // Main Methods 16 | func connect(viewUri: URL, beginTime: Date) 17 | func disconnect() 18 | } 19 | 20 | protocol NdgrClientDelegate: AnyObject { 21 | // Main connection sequence. 22 | func ndgrClientDidConnect(_ ndgrClient: NdgrClientType) 23 | func ndgrClientDidReceiveChat(_ ndgrClient: NdgrClientType, chat: Chat) 24 | func ndgrClientDidDisconnect(_ ndgrClient: NdgrClientType) 25 | 26 | // History. 27 | func ndgrClientReceivingChatHistory(_ ndgrClient: NdgrClientType, requestCount: Int, totalChatCount: Int) 28 | func ndgrClientDidReceiveChatHistory(_ ndgrClient: NdgrClientType, chats: [Chat]) 29 | } 30 | -------------------------------------------------------------------------------- /Hakumai/Extensions/AFError+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AFError+Extensions.swift 3 | // Hakumai 4 | // 5 | // Created by Hiroyuki Onishi on 2021/06/08. 6 | // Copyright © 2021 Hiroyuki Onishi. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Alamofire 11 | 12 | extension AFError { 13 | var isInvalidToken: Bool { 14 | switch self { 15 | case .responseValidationFailed(reason: let reason): 16 | switch reason { 17 | case .unacceptableStatusCode(code: let code): 18 | return code == 401 19 | default: 20 | break 21 | } 22 | default: 23 | break 24 | } 25 | return false 26 | } 27 | 28 | var isNetworkError: Bool { 29 | switch self { 30 | case .sessionTaskFailed(let error as NSError): 31 | // https://qiita.com/akatsuki174/items/1b8c46253fa2231d2414 32 | return error.domain == NSURLErrorDomain 33 | default: 34 | return false 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Hakumai/Controllers/CustomView/AppearanceMonitorView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppearanceMonitorView.swift 3 | // Hakumai 4 | // 5 | // Created by Hiroyuki Onishi on 2022/01/05. 6 | // Copyright © 2022 Hiroyuki Onishi. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import AppKit 11 | 12 | // Based on https://qiita.com/hituziando/items/f870e67dfb41e0b3b7bb . 13 | final class AppearanceMonitorView: NSView { 14 | private var onAppearanceChanged: (() -> Void)? 15 | 16 | public static func make(onAppearanceChanged: @escaping () -> Void) -> AppearanceMonitorView { 17 | let view = AppearanceMonitorView(frame: .zero) 18 | view.onAppearanceChanged = onAppearanceChanged 19 | return view 20 | } 21 | 22 | override func viewDidMoveToSuperview() { 23 | let added = superview != nil 24 | log.debug("[\(self.className)] Monitoring \(added ? "started" : "stopped").") 25 | } 26 | 27 | @available(OSX 10.14, *) 28 | override func viewDidChangeEffectiveAppearance() { 29 | onAppearanceChanged?() 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Hakumai/Controllers/CustomView/CircleImageView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CircleImageView.swift 3 | // Hakumai 4 | // 5 | // Created by Hiroyuki Onishi on 2021/10/16. 6 | // Copyright © 2021 Hiroyuki Onishi. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import AppKit 11 | 12 | final class CircleImageView: NSImageView { 13 | override var frame: NSRect { 14 | didSet { 15 | super.frame = frame 16 | adjustCornerRadius() 17 | } 18 | } 19 | 20 | // Seems `override var bounds` is not called even when the size of image view changed. 21 | 22 | override func awakeFromNib() { 23 | configure() 24 | } 25 | } 26 | 27 | private extension CircleImageView { 28 | func configure() { 29 | adjustCornerRadius() 30 | } 31 | 32 | func adjustCornerRadius() { 33 | let targetRadius = min(bounds.width, bounds.height) / 2 34 | guard layer?.cornerRadius != targetRadius else { return } 35 | wantsLayer = true 36 | layer?.cornerRadius = targetRadius 37 | layer?.masksToBounds = true 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Hakumai/Models/Application/Live.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Live.swift 3 | // Hakumai 4 | // 5 | // Created by Hiroyuki Onishi on 11/19/14. 6 | // Copyright (c) 2014 Hiroyuki Onishi. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | private let liveBaseUrl = "http://live.nicovideo.jp/watch/" 12 | 13 | struct Live { 14 | // MARK: - Properties 15 | // "lv" prefix is included in live id like "lv12345" 16 | let liveProgramId: String 17 | let title: String 18 | let baseTime: Date 19 | let openTime: Date 20 | let beginTime: Date 21 | let isTimeShift: Bool 22 | let programProvider: ProgramProvider 23 | 24 | var liveUrlString: String { liveBaseUrl + liveProgramId } 25 | } 26 | 27 | extension Live: CustomStringConvertible { 28 | var description: String { 29 | "Live: liveId[\(liveProgramId)] title[\(title)] " + 30 | "baseTime[\(baseTime.description)] openTime[\(openTime.description)] " + 31 | "beginTime[\(beginTime.description)] isTimeShift[\(isTimeShift)] " + 32 | "programProvider[\(programProvider)]" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [![Swift 5.5](https://img.shields.io/badge/Swift-5.5-orange.svg?style=flat)](https://swift.org/) 4 | [![License MIT](https://img.shields.io/badge/License-MIT-lightgrey.svg?style=flat)](http://www.opensource.org/licenses/mit-license.php) 5 | [![Build Status](https://github.com/honishi/Hakumai/workflows/Build%20and%20Run%20Tests/badge.svg?branch=main)](https://github.com/honishi/Hakumai/actions) 6 | 7 | * Niconama comment viewer alternative for macOS. 8 | * Download available at [https://honishi.github.io/Hakumai](https://honishi.github.io/Hakumai). 9 | * Server implementation is available at [https://github.com/honishi/Hakumai-server](https://github.com/honishi/Hakumai-server). 10 | 11 | 12 | 13 | Project setup 14 | -- 15 | ```` 16 | bundle install --path=vendor/bundle 17 | bundle exec pod install 18 | open Hakumai.xcworkspace 19 | ```` 20 | 21 | License 22 | -- 23 | Copyright © 2015- honishi, Hiroyuki Onishi. 24 | 25 | Distributed under the [MIT license](http://www.opensource.org/licenses/mit-license.php). 26 | -------------------------------------------------------------------------------- /HakumaiTests/Models/ChatMessageTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChatMessageTests.swift 3 | // Hakumai 4 | // 5 | // Created by Hiroyuki Onishi on 1/4/15. 6 | // Copyright (c) 2015 Hiroyuki Onishi. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import XCTest 11 | @testable import Hakumai 12 | 13 | final class ChatMessageTests: XCTestCase { 14 | override func setUp() { 15 | super.setUp() 16 | } 17 | 18 | override func tearDown() { 19 | super.tearDown() 20 | } 21 | 22 | func testRemoveHtmlTags() { 23 | var comment = "" 24 | var expected = "" 25 | var actual = "" 26 | let caster = Premium.caster 27 | 28 | comment = "https://example.com/watch/xxx?abc" 29 | expected = "https://example.com/watch/xxx?abc" 30 | actual = comment.htmlTagRemoved(premium: caster) 31 | XCTAssert(expected == actual, "") 32 | 33 | comment = "bbb" 34 | expected = "bbb" 35 | actual = comment.htmlTagRemoved(premium: caster) 36 | XCTAssert(expected == actual, "") 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 honishi, Hiroyuki Onishi 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 | -------------------------------------------------------------------------------- /Hakumai/Extensions/NSWindow+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSWindowExtension.swift 3 | // Hakumai 4 | // 5 | // Created by Hiroyuki Onishi on 12/15/14. 6 | // Copyright (c) 2014 Hiroyuki Onishi. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import AppKit 11 | 12 | private let windowLevelKeyForNormal: CGWindowLevelKey = .normalWindow 13 | private let windowLevelKeyForAlwaysOnTop: CGWindowLevelKey = .floatingWindow 14 | 15 | extension NSWindow { 16 | // http://qiita.com/rryu/items/04af65d772e81d2beb7a 17 | var alwaysOnTop: Bool { 18 | get { 19 | let windowLevel = Int(CGWindowLevelForKey(windowLevelKeyForAlwaysOnTop)) 20 | return level.rawValue == windowLevel 21 | } 22 | 23 | set(newAlwaysOnTop) { 24 | let windowLevelKey = newAlwaysOnTop ? windowLevelKeyForAlwaysOnTop : windowLevelKeyForNormal 25 | let windowLevel = Int(CGWindowLevelForKey(windowLevelKey)) 26 | level = NSWindow.Level(rawValue: windowLevel) 27 | // `.managed` to keep window being within spaces(mission control) even if special window level 28 | collectionBehavior = .managed 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Hakumai/Extensions/L10n+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // L10n+Extensions.swift 3 | // Hakumai 4 | // 5 | // Created by Hiroyuki Onishi on 2021/06/03. 6 | // Copyright © 2021 Hiroyuki Onishi. All rights reserved. 7 | // 8 | 9 | // 10 | // L10n extension Based on https://qiita.com/mogmet/items/67bae14f021f9734365e 11 | // 12 | import Foundation 13 | import Cocoa 14 | 15 | private let fatalErrorMessageForGetter = "only set this value" 16 | 17 | extension NSMenu { 18 | @IBInspectable 19 | private var localizedKey: String? { 20 | get { fatalError(fatalErrorMessageForGetter) } 21 | set { title = newValue?.localized ?? "" } 22 | } 23 | } 24 | 25 | extension NSMenuItem { 26 | @IBInspectable 27 | private var localizedKey: String? { 28 | get { fatalError(fatalErrorMessageForGetter) } 29 | set { title = newValue?.localized ?? "" } 30 | } 31 | } 32 | 33 | extension NSTextField { 34 | @IBInspectable 35 | private var localizedKey: String? { 36 | get { fatalError(fatalErrorMessageForGetter) } 37 | set { stringValue = newValue?.localized ?? "" } 38 | } 39 | } 40 | 41 | extension String { 42 | var localized: String? { NSLocalizedString(self, comment: "") } 43 | } 44 | -------------------------------------------------------------------------------- /Hakumai/Managers/BrowserUrlObserver/IgnoreLiveRegistry.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IgnoreUrlRegistry.swift 3 | // Hakumai 4 | // 5 | // Created by Hiroyuki Onishi on 2022/06/10. 6 | // Copyright © 2022 Hiroyuki Onishi. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | final class IgnoreLiveRegistry { 12 | struct IgnoreLive { 13 | let untilDate: Date 14 | let liveProgramId: String 15 | } 16 | 17 | private var ignoreLives: [IgnoreLive] = [] 18 | } 19 | 20 | extension IgnoreLiveRegistry: IgnoreLiveRegistryType { 21 | func add(liveProgramId: String, seconds: TimeInterval) { 22 | refresh() 23 | ignoreLives.append( 24 | IgnoreLive( 25 | untilDate: Date().addingTimeInterval(seconds), 26 | liveProgramId: liveProgramId 27 | ) 28 | ) 29 | } 30 | 31 | func shouldIgnore(liveProgramId: String) -> Bool { 32 | refresh() 33 | return ignoreLives.map({ $0.liveProgramId }).contains(liveProgramId) 34 | } 35 | } 36 | 37 | private extension IgnoreLiveRegistry { 38 | func refresh() { 39 | let origin = Date() 40 | ignoreLives = ignoreLives 41 | .filter { $0.untilDate.timeIntervalSince(origin) > 0 } 42 | // log.debug(ignoreLives) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Hakumai/Controllers/MainWindowController/TableCellView/ColoredView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColoredView.swift 3 | // Hakumai 4 | // 5 | // Created by Hiroyuki Onishi on 11/25/14. 6 | // Copyright (c) 2014 Hiroyuki Onishi. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import AppKit 11 | import QuartzCore 12 | 13 | final class ColoredView: NSView, CALayerDelegate { 14 | var fillColor: NSColor = NSColor.gray { didSet { layer?.setNeedsDisplay() } } 15 | 16 | override init(frame: CGRect) { 17 | super.init(frame: frame) 18 | } 19 | 20 | required init?(coder: NSCoder) { 21 | super.init(coder: coder) 22 | } 23 | } 24 | 25 | extension ColoredView { 26 | override func awakeFromNib() { 27 | // calyer implementation based on http://rway.tumblr.com/post/4525503228 28 | let layer = CALayer() 29 | layer.delegate = self 30 | layer.bounds = bounds 31 | layer.needsDisplayOnBoundsChange = true 32 | layer.setNeedsDisplay() 33 | 34 | self.layer = layer 35 | wantsLayer = true 36 | } 37 | } 38 | 39 | extension ColoredView { 40 | override func draw(_ dirtyRect: NSRect) { 41 | // http://stackoverflow.com/a/2962882 42 | fillColor.setFill() 43 | dirtyRect.fill() 44 | super.draw(dirtyRect) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Hakumai/Models/Application/Chat.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Chat.swift 3 | // Hakumai 4 | // 5 | // Created by Hiroyuki Onishi on 11/19/14. 6 | // Copyright (c) 2014 Hiroyuki Onishi. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct Chat { 12 | // MARK: - Properties 13 | let roomPosition: RoomPosition 14 | let no: Int 15 | let date: Date 16 | let dateUsec: Int 17 | let mail: [String]? 18 | let userId: String 19 | let comment: String 20 | let premium: Premium 21 | // ndgr 暫定対応のための仮プロパティ 22 | let chatType: ChatType 23 | 24 | var isComment: Bool { 25 | switch chatType { 26 | case .comment: 27 | return true 28 | case .gift, .nicoad, .other: 29 | return false 30 | } 31 | } 32 | 33 | var isDisconnect: Bool { premium == .system && comment == "/disconnect" } 34 | } 35 | 36 | enum ChatType { 37 | case comment 38 | case gift(imageUrl: URL) 39 | case nicoad 40 | case other 41 | } 42 | 43 | extension Chat: CustomStringConvertible { 44 | var description: String { 45 | "Chat: roomPosition[\(roomPosition)] no[\(no)] " + 46 | "date[\(date.description)] dateUsec[\(dateUsec)] mail[\(mail ?? [])] userId[\(userId)] " + 47 | "premium[\(premium.description)] comment[\(comment) chatType[\(String(describing: chatType))]]" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /HakumaiTests/Models/DatabaseValueCacherTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DatabaseValueCacherTests.swift 3 | // HakumaiTests 4 | // 5 | // Created by Hiroyuki Onishi on 2021/12/28. 6 | // Copyright © 2021 Hiroyuki Onishi. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Hakumai 11 | 12 | class DatabaseValueCacherTests: XCTestCase { 13 | override func setUpWithError() throws {} 14 | override func tearDownWithError() throws {} 15 | 16 | func testExample() throws { 17 | let cacher = DatabaseValueCacher() 18 | let userId1 = "userId1" 19 | let communityId1 = "communityId1" 20 | // let userId2 = "userId2" 21 | // let communityId2 = "communityId2" 22 | 23 | var actual: DatabaseValueCacher.CacheStatus 24 | var expected: DatabaseValueCacher.CacheStatus 25 | 26 | actual = cacher.cachedValue(for: userId1, in: communityId1) 27 | expected = .notCached 28 | XCTAssert(actual == expected) 29 | 30 | cacher.update(value: "abc", for: userId1, in: communityId1) 31 | actual = cacher.cachedValue(for: userId1, in: communityId1) 32 | expected = .cached("abc") 33 | XCTAssert(actual == expected) 34 | 35 | cacher.updateValueAsNil(for: userId1, in: communityId1) 36 | actual = cacher.cachedValue(for: userId1, in: communityId1) 37 | expected = .cached(nil) 38 | XCTAssert(actual == expected) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Hakumai/Controllers/MainWindowController/TableCellView/IconTableCellView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IconTableCellView.swift 3 | // Hakumai 4 | // 5 | // Created by Hiroyuki Onishi on 2021/10/09. 6 | // Copyright © 2021 Hiroyuki Onishi. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import Kingfisher 11 | 12 | enum IconType { 13 | case none 14 | case user(URL?) 15 | } 16 | 17 | final class IconTableCellView: NSTableCellView { 18 | @IBOutlet weak var iconImageView: CircleImageView! 19 | 20 | override func prepareForReuse() {} 21 | } 22 | 23 | extension IconTableCellView { 24 | func configure(iconType: IconType) { 25 | reset() 26 | switch iconType { 27 | case .none: 28 | // no-op. 29 | break 30 | case .user(let iconUrl): 31 | guard let iconUrl = iconUrl else { return } 32 | let retry = DelayRetryStrategy(maxRetryCount: 3, retryInterval: .seconds(1)) 33 | iconImageView.kf.setImage( 34 | with: iconUrl, 35 | placeholder: Asset.defaultUserImage.image, 36 | options: [.retryStrategy(retry)] 37 | ) 38 | } 39 | } 40 | } 41 | 42 | private extension IconTableCellView { 43 | func reset() { 44 | // https://stackoverflow.com/a/62006790/13220031 45 | iconImageView.kf.cancelDownloadTask() 46 | iconImageView.kf.setImage(with: URL(string: "")) 47 | iconImageView.image = nil 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Hakumai/Managers/HandleNameManager/DatabaseValueCacher.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DatabaseValueCacher.swift 3 | // Hakumai 4 | // 5 | // Created by Hiroyuki Onishi on 2021/12/28. 6 | // Copyright © 2021 Hiroyuki Onishi. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class DatabaseValueCacher { 12 | enum CacheStatus: Equatable { 13 | case cached(T?) // `.cached(nil)` means the value is cached as `nil`. 14 | case notCached 15 | } 16 | 17 | private var cache: [String: CacheStatus] = [:] 18 | 19 | func update(value: T?, for userId: String, in communityId: String) { 20 | objc_sync_enter(self) 21 | defer { objc_sync_exit(self) } 22 | let key = cacheKey(userId, communityId) 23 | let _value: CacheStatus = { 24 | guard let value = value else { return .cached(nil) } 25 | return .cached(value) 26 | }() 27 | cache[key] = _value 28 | } 29 | 30 | func updateValueAsNil(for userId: String, in communityId: String) { 31 | update(value: nil, for: userId, in: communityId) 32 | } 33 | 34 | func cachedValue(for userId: String, in communityId: String) -> CacheStatus { 35 | objc_sync_enter(self) 36 | defer { objc_sync_exit(self) } 37 | let key = cacheKey(userId, communityId) 38 | return cache[key] ?? .notCached 39 | } 40 | 41 | private func cacheKey(_ userId: String, _ communityId: String) -> String { 42 | return "\(userId):\(communityId)" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /protobuf/nicolive-comment-protobuf/dwango/nicolive/chat/data/atoms/moderator.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package dwango.nicolive.chat.data.atoms; 4 | 5 | import "google/protobuf/timestamp.proto"; 6 | 7 | 8 | message ModeratorUserInfo { 9 | int64 user_id = 1; 10 | optional string nickname = 2; 11 | optional string iconUrl = 3; 12 | } 13 | 14 | 15 | message ModeratorUpdated { 16 | enum ModeratorOperation { 17 | ADD = 0; 18 | DELETE = 1; 19 | } 20 | 21 | ModeratorOperation operation = 1; 22 | 23 | ModeratorUserInfo operator = 2; 24 | 25 | google.protobuf.Timestamp updatedAt = 3; 26 | } 27 | 28 | 29 | message SSNGUpdated { 30 | enum SSNGOperation { 31 | ADD = 0; 32 | DELETE = 1; 33 | } 34 | enum SSNGType { 35 | USER = 0; 36 | WORD = 1; 37 | COMMAND = 2; 38 | } 39 | 40 | SSNGOperation operation = 1; 41 | 42 | int64 ssng_id = 2; 43 | 44 | ModeratorUserInfo operator = 3; 45 | 46 | optional SSNGType type = 4; 47 | 48 | optional string source = 5; 49 | 50 | optional google.protobuf.Timestamp updatedAt = 6; 51 | } 52 | 53 | 54 | message ModerationAnnouncement { 55 | enum GuidelineItem { 56 | UNKNOWN = 0; 57 | SEXUAL = 1; 58 | SPAM = 2; 59 | SLANDER = 3; 60 | PERSONAL_INFORMATION = 4; 61 | } 62 | 63 | optional string message = 1; 64 | 65 | repeated GuidelineItem guidelineItems = 2; 66 | 67 | google.protobuf.Timestamp updatedAt = 3; 68 | } 69 | 70 | -------------------------------------------------------------------------------- /Hakumai/Global.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Global.swift 3 | // Hakumai 4 | // 5 | // Created by Hiroyuki Onishi on 12/22/14. 6 | // Copyright (c) 2014 Hiroyuki Onishi. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import XCGLogger 11 | 12 | // global logger 13 | let log = XCGLogger.default 14 | 15 | // MARK: Constant value for storyboard contents 16 | let kNibNameRoomPositionTableCellView = "RoomPositionTableCellView" 17 | let kNibNameTimeTableCellView = "TimeTableCellView" 18 | let kNibNameIconTableCellView = "IconTableCellView" 19 | let kNibNameUserIdTableCellView = "UserIdTableCellView" 20 | let kNibNameCommentTableCellView = "CommentTableCellView" 21 | let kNibNamePremiumTableCellView = "PremiumTableCellView" 22 | 23 | let kRoomPositionColumnIdentifier = "RoomPositionColumn" 24 | let kTimeColumnIdentifier = "TimeColumn" 25 | let kIconColumnIdentifier = "IconColumn" 26 | let kCommentColumnIdentifier = "CommentColumn" 27 | let kUserIdColumnIdentifier = "UserIdColumn" 28 | let kPremiumColumnIdentifier = "PremiumColumn" 29 | 30 | // MARK: Common regular expressions 31 | // mail address regular expression based on http://qiita.com/sakuro/items/1eaa307609ceaaf51123 32 | let kRegexpMailAddress = "[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\\.[a-zA-Z0-9-]+)*" 33 | 34 | // MARK: font size 35 | let kDefaultFontSize: Float = 13 36 | let kMinimumFontSize: Float = 9 37 | let kMaximumFontSize: Float = 30 38 | 39 | // MARK: Common constants 40 | let commonUserAgentKey = "User-Agent" 41 | let commonUserAgentValue = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36" 42 | -------------------------------------------------------------------------------- /Hakumai/Controllers/MainWindowController/TableCellView/PremiumTableCellView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PremiumTableCellView.swift 3 | // Hakumai 4 | // 5 | // Created by Hiroyuki Onishi on 12/7/14. 6 | // Copyright (c) 2014 Hiroyuki Onishi. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import AppKit 11 | 12 | final class PremiumTableCellView: NSTableCellView { 13 | @IBOutlet weak var premiumImageView: NSImageView! 14 | @IBOutlet weak var premiumTextField: NSTextField! 15 | 16 | var fontSize: CGFloat? { didSet { set(fontSize: fontSize) } } 17 | } 18 | 19 | extension PremiumTableCellView { 20 | func configure(premium: Premium?) { 21 | set(premium: premium) 22 | } 23 | } 24 | 25 | private extension PremiumTableCellView { 26 | func set(premium: Premium?) { 27 | guard let premium = premium else { 28 | premiumImageView.image = nil 29 | premiumTextField.stringValue = "" 30 | return 31 | } 32 | premiumImageView.image = image(forPremium: premium) 33 | premiumTextField.stringValue = premium.label() 34 | } 35 | 36 | func image(forPremium premium: Premium) -> NSImage { 37 | switch premium { 38 | case .premium: 39 | return Asset.premiumPremium.image 40 | case .ippan, .ippanTransparent: 41 | return Asset.premiumIppan.image 42 | case .system, .caster, .operator, .bsp: 43 | return Asset.premiumMisc.image 44 | } 45 | } 46 | 47 | func set(fontSize: CGFloat?) { 48 | let size = fontSize ?? CGFloat(kDefaultFontSize) 49 | premiumTextField.font = NSFont.systemFont(ofSize: size) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /.github/workflows/build-test.yml: -------------------------------------------------------------------------------- 1 | name: Build and Run Tests 2 | 3 | on: [ push, pull_request ] 4 | 5 | jobs: 6 | build: 7 | runs-on: macos-14 8 | 9 | # checkout 10 | steps: 11 | - uses: actions/checkout@v2 12 | 13 | # debug prints 14 | - name: Show Xcode versions 15 | run: | 16 | xcodebuild -version 17 | ls /Applications | grep 'Xcode' 18 | 19 | # setup ruby and bundler 20 | - name: Setup Ruby 21 | uses: ruby/setup-ruby@v1 22 | with: 23 | ruby-version: 2.7 24 | bundler-cache: true 25 | - name: Check Ruby version 26 | run: ruby -v 27 | 28 | # setup xcode version 29 | - name: Setup Xcode 30 | uses: maxim-lobanov/setup-xcode@v1 31 | with: 32 | xcode-version: '15.4' 33 | 34 | # setup xcode project 35 | - name: Setup cache for Pods 36 | uses: actions/cache@v3 37 | with: 38 | path: Pods 39 | key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }} 40 | - name: Pod install 41 | run: | 42 | # bundle exec pod repo update --silent 43 | bundle exec pod install 44 | 45 | # before test 46 | - name: Copy sample credential file 47 | run: cp ./Hakumai/OAuthCredential.sample.swift ./Hakumai/OAuthCredential.swift 48 | 49 | # build and run tests 50 | - name: Build and Run Tests 51 | run: | 52 | xcodebuild -version 53 | set -o pipefail && xcodebuild -workspace Hakumai.xcworkspace -scheme Hakumai -configuration Debug -destination 'platform=OS X' CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO test | bundle exec xcpretty -c 54 | -------------------------------------------------------------------------------- /Hakumai/Extensions/NSView+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSView+Extensions.swift 3 | // Hakumai 4 | // 5 | // Created by Hiroyuki Onishi on 2021/05/26. 6 | // Copyright © 2021 Hiroyuki Onishi. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Cocoa 11 | 12 | extension NSView { 13 | func addBorder() { 14 | // use async to properly render border line. if not async, the line sometimes disappears 15 | DispatchQueue.main.async { self._addBorder() } 16 | } 17 | 18 | func _addBorder() { 19 | layer?.borderWidth = 0.5 20 | layer?.masksToBounds = true 21 | layer?.borderColor = NSColor.black.cgColor 22 | } 23 | 24 | func enableCornerRadius(_ radius: CGFloat = 4) { 25 | guard layer?.cornerRadius != radius else { return } 26 | wantsLayer = true 27 | layer?.cornerRadius = radius 28 | layer?.masksToBounds = true 29 | } 30 | } 31 | 32 | private let keyPathBackgroundColor = "backgroundColor" 33 | 34 | extension NSView { 35 | func setBackgroundColor(_ color: NSColor?) { 36 | // https://stackoverflow.com/a/17795052/13220031 37 | wantsLayer = true 38 | layer?.backgroundColor = color?.cgColor 39 | } 40 | 41 | func flash(_ color: NSColor, duration: TimeInterval = 1) { 42 | cancelFlash() 43 | 44 | wantsLayer = true 45 | let animation = CABasicAnimation(keyPath: keyPathBackgroundColor) 46 | animation.fromValue = color.cgColor 47 | animation.toValue = NSColor.clear.cgColor 48 | animation.duration = duration 49 | layer?.add(animation, forKey: animation.keyPath) 50 | } 51 | 52 | func cancelFlash() { 53 | layer?.removeAnimation(forKey: keyPathBackgroundColor) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Hakumai/Managers/BrowserUrlObserver/BrowserUrlObserver.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BrowserUrlObserver.swift 3 | // Hakumai 4 | // 5 | // Created by Hiroyuki Onishi on 2021/12/07. 6 | // Copyright © 2021 Hiroyuki Onishi. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | private let timerInterval: TimeInterval = 2 12 | 13 | final class BrowserUrlObserver { 14 | private(set) var browser: BrowserInUseType = .chrome 15 | private(set) weak var delegate: BrowserUrlObserverDelegate? 16 | private var timer: Timer? 17 | } 18 | 19 | extension BrowserUrlObserver: BrowserUrlObserverType { 20 | func setBrowserType(_ browser: BrowserInUseType) { 21 | self.browser = browser 22 | log.debug("set browser: \(browser)") 23 | } 24 | 25 | func start(delegate: BrowserUrlObserverDelegate) { 26 | self.delegate = delegate 27 | scheduleTimer() 28 | } 29 | 30 | func stop() { 31 | invalidateTimer() 32 | } 33 | } 34 | 35 | private extension BrowserUrlObserver { 36 | func scheduleTimer() { 37 | timer = Timer.scheduledTimer( 38 | timeInterval: timerInterval, 39 | target: self, 40 | selector: #selector(BrowserUrlObserver.timerFired), 41 | userInfo: nil, 42 | repeats: true) 43 | log.debug("Scheduled timer.") 44 | } 45 | 46 | func invalidateTimer() { 47 | timer?.invalidate() 48 | timer = nil 49 | log.debug("Invalidated timer.") 50 | } 51 | 52 | @objc 53 | func timerFired() { 54 | guard let urlString = BrowserHelper.extractUrl(fromBrowser: browser.toBrowserHelperBrowserType), 55 | urlString.isLiveUrl, 56 | let url = URL(string: urlString) else { return } 57 | delegate?.browserUrlObserver(self, didGetUrl: url) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /docs/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | addressable (2.8.0) 5 | public_suffix (>= 2.0.2, < 5.0) 6 | colorator (1.1.0) 7 | concurrent-ruby (1.1.8) 8 | em-websocket (0.5.2) 9 | eventmachine (>= 0.12.9) 10 | http_parser.rb (~> 0.6.0) 11 | eventmachine (1.2.7) 12 | ffi (1.15.0) 13 | forwardable-extended (2.6.0) 14 | http_parser.rb (0.6.0) 15 | i18n (1.8.10) 16 | concurrent-ruby (~> 1.0) 17 | jekyll (4.2.0) 18 | addressable (~> 2.4) 19 | colorator (~> 1.0) 20 | em-websocket (~> 0.5) 21 | i18n (~> 1.0) 22 | jekyll-sass-converter (~> 2.0) 23 | jekyll-watch (~> 2.0) 24 | kramdown (~> 2.3) 25 | kramdown-parser-gfm (~> 1.0) 26 | liquid (~> 4.0) 27 | mercenary (~> 0.4.0) 28 | pathutil (~> 0.9) 29 | rouge (~> 3.0) 30 | safe_yaml (~> 1.0) 31 | terminal-table (~> 2.0) 32 | jekyll-sass-converter (2.1.0) 33 | sassc (> 2.0.1, < 3.0) 34 | jekyll-watch (2.2.1) 35 | listen (~> 3.0) 36 | kramdown (2.3.1) 37 | rexml 38 | kramdown-parser-gfm (1.1.0) 39 | kramdown (~> 2.0) 40 | liquid (4.0.3) 41 | listen (3.5.1) 42 | rb-fsevent (~> 0.10, >= 0.10.3) 43 | rb-inotify (~> 0.9, >= 0.9.10) 44 | mercenary (0.4.0) 45 | pathutil (0.16.2) 46 | forwardable-extended (~> 2.6) 47 | public_suffix (4.0.6) 48 | rb-fsevent (0.10.4) 49 | rb-inotify (0.10.1) 50 | ffi (~> 1.0) 51 | rexml (3.3.9) 52 | rouge (3.26.0) 53 | safe_yaml (1.0.5) 54 | sassc (2.4.0) 55 | ffi (~> 1.9) 56 | terminal-table (2.0.0) 57 | unicode-display_width (~> 1.1, >= 1.1.1) 58 | unicode-display_width (1.7.0) 59 | 60 | PLATFORMS 61 | arm64-darwin-20 62 | ruby 63 | 64 | DEPENDENCIES 65 | jekyll 66 | 67 | BUNDLED WITH 68 | 2.2.15 69 | -------------------------------------------------------------------------------- /Hakumai/Controllers/CustomView/ClickTableView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UnclickableTableView.swift 3 | // Hakumai 4 | // 5 | // Created by Hiroyuki Onishi on 2021/09/15. 6 | // Copyright © 2021 Hiroyuki Onishi. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import AppKit 11 | 12 | final class ClickTableView: NSTableView { 13 | private var clickHandler: (() -> Void)? 14 | private var doubleClickHandler: (() -> Void)? 15 | private var lastClickedRow = -1 16 | } 17 | 18 | extension ClickTableView { 19 | override func awakeFromNib() { 20 | configure() 21 | } 22 | 23 | func setClickAction(clickHandler: (() -> Void)? = nil, doubleClickHandler: (() -> Void)? = nil) { 24 | self.clickHandler = clickHandler 25 | self.doubleClickHandler = doubleClickHandler 26 | } 27 | 28 | @objc func rowClicked(_ sender: AnyObject?) { 29 | // log.debug("\(clickedRow), \(selectedRow)") 30 | guard let clickHandler = clickHandler else { 31 | unclickRow() 32 | return 33 | } 34 | clickHandler() 35 | } 36 | 37 | @objc func rowDoubleClicked(_ sender: AnyObject?) { 38 | // log.debug("\(clickedRow), \(selectedRow)") 39 | guard let doubleClickHandler = doubleClickHandler else { return } 40 | doubleClickHandler() 41 | } 42 | } 43 | 44 | private extension ClickTableView { 45 | func configure() { 46 | target = self 47 | action = #selector(ClickTableView.rowClicked(_:)) 48 | doubleAction = #selector(ClickTableView.rowDoubleClicked(_:)) 49 | } 50 | 51 | func unclickRow() { 52 | guard clickedRow != -1 else { return } 53 | if lastClickedRow == clickedRow { 54 | deselectRow(clickedRow) 55 | lastClickedRow = -1 56 | } else { 57 | lastClickedRow = clickedRow 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Hakumai/Controllers/AuthWindowController/AuthWindowController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthWindowController.swift 3 | // Hakumai 4 | // 5 | // Created by Hiroyuki Onishi on 2021/05/20. 6 | // Copyright © 2021 Hiroyuki Onishi. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | protocol AuthWindowControllerDelegate: AnyObject { 12 | func authWindowControllerDidLogin(_ authWindowController: AuthWindowController) 13 | } 14 | 15 | extension AuthWindowController { 16 | static func make(delegate: AuthWindowControllerDelegate?) -> AuthWindowController { 17 | let wc = StoryboardScene.AuthWindowController.authWindowController.instantiate() 18 | wc.delegate = delegate 19 | return wc 20 | } 21 | } 22 | 23 | final class AuthWindowController: NSWindowController { 24 | // MARK: - Properties 25 | private weak var delegate: AuthWindowControllerDelegate? 26 | 27 | private var authViewController: AuthViewController? { contentViewController as? AuthViewController } 28 | 29 | // MARK: - Object Lifecycle 30 | deinit { log.debug("deinit") } 31 | 32 | override func windowDidLoad() { 33 | super.windowDidLoad() 34 | window?.delegate = self 35 | authViewController?.setDelegate(self) 36 | } 37 | } 38 | 39 | extension AuthWindowController: NSWindowDelegate { 40 | func windowWillClose(_ notification: Notification) { 41 | log.debug("will close") 42 | } 43 | } 44 | 45 | extension AuthWindowController: AuthViewControllerDelegate { 46 | func authViewControllerDidLogin(_ authViewController: AuthViewController) { 47 | delegate?.authWindowControllerDidLogin(self) 48 | } 49 | } 50 | 51 | extension AuthWindowController { 52 | func startAuthorization() { 53 | authViewController?.startAuthorization() 54 | } 55 | 56 | func logout() { 57 | authViewController?.clearAllCookies() 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Hakumai/Controllers/PreferenceWindowController/MuteAddViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MuteAddViewController.swift 3 | // Hakumai 4 | // 5 | // Created by Hiroyuki Onishi on 12/28/14. 6 | // Copyright (c) 2014 Hiroyuki Onishi. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import AppKit 11 | 12 | final class MuteAddViewController: NSViewController { 13 | // MARK: - Properties 14 | @IBOutlet private weak var titleLabel: NSTextField! 15 | @IBOutlet private weak var cancelButton: NSButton! 16 | @IBOutlet private weak var addButton: NSButton! 17 | 18 | // this property contains mute target value, and is also used binding between text filed and add button. 19 | // http://stackoverflow.com/a/24017991 20 | // also see more detailed note in HandleNameAddViewController's propery 21 | @objc dynamic var muteValue: NSString! 22 | 23 | var completion: ((_ cancelled: Bool, _ muteValue: String?) -> Void)? 24 | 25 | // MARK: - Object Lifecycle 26 | required init?(coder: NSCoder) { 27 | super.init(coder: coder) 28 | } 29 | 30 | deinit { log.debug("") } 31 | } 32 | 33 | extension MuteAddViewController { 34 | override func viewDidLoad() { 35 | super.viewDidLoad() 36 | configureView() 37 | } 38 | } 39 | 40 | // MARK: - Internal Functions 41 | extension MuteAddViewController { 42 | // MARK: Button Handlers 43 | @IBAction func addMute(_ sender: AnyObject) { 44 | guard 0 < muteValue.length else { return } 45 | completion?(false, muteValue as String) 46 | } 47 | 48 | @IBAction func cancelAddMute(_ sender: AnyObject) { 49 | completion?(true, nil) 50 | } 51 | } 52 | 53 | private extension MuteAddViewController { 54 | func configureView() { 55 | titleLabel.stringValue = "\(L10n.enterMuteUserIdWord):" 56 | cancelButton.title = L10n.cancel 57 | addButton.title = L10n.add 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /HakumaiTests/Models/NicoManagerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NicoManagerTests.swift 3 | // Hakumai 4 | // 5 | // Created by Hiroyuki Onishi on 11/14/14. 6 | // Copyright (c) 2014 Hiroyuki Onishi. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import XCTest 11 | @testable import Hakumai 12 | 13 | private let kAsyncTimeout: TimeInterval = 3 14 | 15 | final class NicoManagerTests: XCTestCase { 16 | override func setUp() { 17 | super.setUp() 18 | } 19 | 20 | override func tearDown() { 21 | super.tearDown() 22 | } 23 | 24 | // MARK: - User Account 25 | func testUserIcon() { 26 | let nicoManager: NicoManagerType = NicoManager() 27 | var expected: String? = "" 28 | var actual: String? = "" 29 | 30 | expected = nil 31 | actual = nicoManager.userIconUrl(for: "XXX")?.absoluteString 32 | XCTAssert(actual == expected) 33 | 34 | expected = "https://secure-dcdn.cdn.nimg.jp/nicoaccount/usericon/0/2.jpg" 35 | actual = nicoManager.userIconUrl(for: "2")?.absoluteString 36 | XCTAssert(actual == expected) 37 | 38 | expected = "https://secure-dcdn.cdn.nimg.jp/nicoaccount/usericon/0/9005.jpg" 39 | actual = nicoManager.userIconUrl(for: "9005")?.absoluteString 40 | XCTAssert(actual == expected) 41 | 42 | expected = "https://secure-dcdn.cdn.nimg.jp/nicoaccount/usericon/9/99998.jpg" 43 | actual = nicoManager.userIconUrl(for: "99998")?.absoluteString 44 | XCTAssert(actual == expected) 45 | 46 | expected = "https://secure-dcdn.cdn.nimg.jp/nicoaccount/usericon/1/12346.jpg" 47 | actual = nicoManager.userIconUrl(for: "12346")?.absoluteString 48 | XCTAssert(actual == expected) 49 | 50 | expected = "https://secure-dcdn.cdn.nimg.jp/nicoaccount/usericon/25/252346.jpg" 51 | actual = nicoManager.userIconUrl(for: "252346")?.absoluteString 52 | XCTAssert(actual == expected) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Hakumai/Controllers/MainWindowController/TableCellView/RoomPositionTableCellView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RoomPositionTableCellView.swift 3 | // Hakumai 4 | // 5 | // Created by Hiroyuki Onishi on 11/25/14. 6 | // Copyright (c) 2014 Hiroyuki Onishi. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import AppKit 11 | 12 | final class RoomPositionTableCellView: NSTableCellView { 13 | @IBOutlet weak var coloredView: ColoredView! 14 | @IBOutlet weak var commentNoLabel: NSTextField! 15 | 16 | var fontSize: CGFloat? { didSet { set(fontSize: fontSize) } } 17 | } 18 | 19 | extension RoomPositionTableCellView { 20 | func configure(message: Message?) { 21 | coloredView.fillColor = color(for: message) 22 | commentNoLabel.stringValue = string(for: message) 23 | } 24 | } 25 | 26 | private extension RoomPositionTableCellView { 27 | func color(for message: Message?) -> NSColor { 28 | guard let message = message else { return UIHelper.systemMessageBgColor() } 29 | switch message.content { 30 | case .system: 31 | return UIHelper.systemMessageBgColor() 32 | case .chat(let chat): 33 | switch chat.chatType { 34 | case .comment: 35 | return UIHelper.roomColor(roomPosition: chat.roomPosition) 36 | case .gift, .nicoad, .other: 37 | return UIHelper.systemMessageBgColor() 38 | } 39 | case .debug: 40 | return UIHelper.debugMessageBgColor() 41 | } 42 | } 43 | 44 | func string(for message: Message?) -> String { 45 | guard case let .chat(chat) = message?.content else { return "" } 46 | if chat.no == 0 { 47 | return "" 48 | } 49 | return String(chat.no).numberStringWithSeparatorComma() 50 | } 51 | 52 | func set(fontSize: CGFloat?) { 53 | let size = fontSize ?? CGFloat(kDefaultFontSize) 54 | commentNoLabel.font = NSFont.systemFont(ofSize: size) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Hakumai/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(MARKETING_VERSION) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(CURRENT_PROJECT_VERSION) 25 | LSApplicationCategoryType 26 | public.app-category.entertainment 27 | LSMinimumSystemVersion 28 | $(MACOSX_DEPLOYMENT_TARGET) 29 | NSAppTransportSecurity 30 | 31 | NSExceptionDomains 32 | 33 | localhost 34 | 35 | NSIncludesSubdomains 36 | 37 | NSTemporaryExceptionAllowsInsecureHTTPLoads 38 | 39 | 40 | 41 | 42 | NSAppleEventsUsageDescription 43 | Please permit the access right to communiate with browser. 44 | NSHumanReadableCopyright 45 | Copyright © honishi, Hiroyuki Onishi. 46 | All rights reserved. 47 | NSMainStoryboardFile 48 | MainWindowController 49 | NSPrincipalClass 50 | NSApplication 51 | SUEnableAutomaticChecks 52 | 53 | SUFeedURL 54 | https://hakumai.s3.amazonaws.com/appcast.xml 55 | 56 | 57 | -------------------------------------------------------------------------------- /Hakumai/Parameter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Parameter.swift 3 | // Hakumai 4 | // 5 | // Created by Hiroyuki Onishi on 12/9/14. 6 | // Copyright (c) 2014 Hiroyuki Onishi. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // top level parameters 12 | // http://stackoverflow.com/a/26252377 13 | struct Parameters { 14 | // general 15 | static let browserInUse = "BrowserInUse" 16 | 17 | // speech 18 | static let commentSpeechVolume = "CommentSpeechVolume" 19 | static let commentSpeechEnableName = "CommentSpeechEnableName" 20 | static let commentSpeechEnableGift = "CommentSpeechEnableGift" 21 | static let commentSpeechEnableAd = "CommentSpeechEnableAd" 22 | static let commentSpeechVoicevoxSpeaker = "CommentSpeechVoicevoxSpeaker" 23 | 24 | // mute 25 | static let enableMuteUserIds = "EnableMuteUserIds" 26 | static let enableMuteWords = "EnableMuteWords" 27 | static let muteUserIds = "MuteUserIds" 28 | static let muteWords = "MuteWords" 29 | 30 | // misc 31 | static let lastLaunchedApplicationVersion = "LastLaunchedApplicationVersion" 32 | static let alwaysOnTop = "AlwaysOnTop" 33 | static let commentAnonymously = "CommentAnonymously" 34 | static let fontSize = "FontSize" 35 | static let enableBrowserUrlObservation = "EnableBrowserUrlObservation" 36 | static let enableBrowserTabSelectionSync = "EnableBrowserTabSelectionSync" 37 | static let enableLiveNotification = "EnableLiveNotification" 38 | static let enableEmotionMessage = "EnableEmotionMessage" 39 | static let enableDebugMessage = "EnableDebugMessage" 40 | } 41 | 42 | // session management 43 | enum BrowserInUseType: Int { 44 | case chrome = 1001 45 | case safari = 1002 46 | } 47 | 48 | // dictionary keys in MuteUserIds array objects 49 | struct MuteUserIdKey { 50 | static let userId = "UserId" 51 | } 52 | 53 | // dictionary keys in MuteUserWords array objects 54 | struct MuteUserWordKey { 55 | static let word = "Word" 56 | // static let EnableRegexp = "EnableRegexp" 57 | } 58 | -------------------------------------------------------------------------------- /protobuf/nicolive-comment-protobuf/dwango/nicolive/chat/service/edge/payload.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package dwango.nicolive.chat.service.edge; 4 | import "google/protobuf/timestamp.proto"; 5 | import "dwango/nicolive/chat/data/message.proto"; 6 | import "dwango/nicolive/chat/data/state.proto"; 7 | import "dwango/nicolive/chat/data/origin.proto"; 8 | 9 | 10 | 11 | message ChunkedMessage { 12 | message Meta { 13 | 14 | string id = 1; 15 | 16 | google.protobuf.Timestamp at = 2; 17 | 18 | data.NicoliveOrigin origin = 3; 19 | } 20 | Meta meta = 1; 21 | oneof payload { 22 | 23 | data.NicoliveMessage message = 2; 24 | 25 | data.NicoliveState state = 4; 26 | 27 | 28 | 29 | Signal signal = 5; 30 | } 31 | 32 | enum Signal { 33 | 34 | 35 | Flushed = 0; 36 | } 37 | } 38 | 39 | 40 | message PackedSegment { 41 | 42 | repeated ChunkedMessage messages = 1; 43 | 44 | message Next { 45 | string uri = 1; 46 | } 47 | 48 | Next next = 2; 49 | 50 | StateSnapshot snapshot = 3; 51 | 52 | message StateSnapshot { 53 | 54 | string uri = 1; 55 | } 56 | } 57 | 58 | 59 | 60 | message ChunkedEntry { 61 | 62 | 63 | oneof entry { 64 | 65 | BackwardSegment backward = 2; 66 | 67 | 68 | MessageSegment previous = 3; 69 | 70 | 71 | MessageSegment segment = 1; 72 | 73 | 74 | ReadyForNext next = 4; 75 | } 76 | message ReadyForNext { 77 | int64 at = 1; 78 | } 79 | } 80 | 81 | 82 | 83 | 84 | message MessageSegment { 85 | 86 | google.protobuf.Timestamp from = 1; 87 | 88 | 89 | google.protobuf.Timestamp until = 2; 90 | 91 | 92 | 93 | string uri = 3; 94 | } 95 | 96 | 97 | 98 | message BackwardSegment { 99 | google.protobuf.Timestamp until = 1; 100 | 101 | 102 | PackedSegment.Next segment = 2; 103 | 104 | PackedSegment.StateSnapshot snapshot = 3; 105 | } 106 | -------------------------------------------------------------------------------- /Hakumai/Controllers/MainWindowController/TableCellView/CommentTableCellView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CommentTableCellView.swift 3 | // Hakumai 4 | // 5 | // Created by Hiroyuki Onishi on 2021/05/14. 6 | // Copyright © 2021 Hiroyuki Onishi. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import AppKit 11 | import Kingfisher 12 | 13 | private let leadingMargin: CGFloat = 2 14 | private let trailingMargin: CGFloat = 2 15 | private let giftImageViewSize = CGSize(width: 32, height: 32) 16 | private let paddingBetweenGiftImageAndComment: CGFloat = 8 17 | 18 | final class CommentTableCellView: NSTableCellView { 19 | @IBOutlet private weak var giftImageView: NSImageView! 20 | @IBOutlet private weak var commentTextField: NSTextField! 21 | 22 | override func awakeFromNib() { 23 | super.awakeFromNib() 24 | giftImageView.enableCornerRadius(4) 25 | } 26 | } 27 | 28 | extension CommentTableCellView { 29 | static func calculateHeight(text: String, attributes: [NSAttributedString.Key: Any], hasGiftImage: Bool, columnWidth: CGFloat) -> CGFloat { 30 | let commentWidth = columnWidth 31 | - leadingMargin 32 | - trailingMargin 33 | - (hasGiftImage ? giftImageViewSize.width + paddingBetweenGiftImageAndComment : 0) 34 | let commentRect = text.boundingRect( 35 | with: CGSize(width: commentWidth, height: 0), 36 | options: .usesLineFragmentOrigin, 37 | attributes: attributes 38 | ) 39 | // log.debug("\(commentRect.size.width),\(commentRect.size.height)") 40 | let giftImageHeight: CGFloat = hasGiftImage ? giftImageViewSize.height : 0 41 | return max(giftImageHeight, commentRect.size.height) 42 | } 43 | 44 | func configure(attributedString: NSAttributedString?, giftImageUrl: URL? = nil) { 45 | commentTextField.attributedStringValue = attributedString ?? NSAttributedString(string: "-") 46 | giftImageView.image = nil 47 | // XXX: set placeholder image? 48 | giftImageView.kf.setImage(with: giftImageUrl) 49 | giftImageView.isHidden = giftImageUrl == nil 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Hakumai/Controllers/CustomView/LiveThumbnailImageView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LiveThumbnailImageView.swift 3 | // Hakumai 4 | // 5 | // Created by Hiroyuki Onishi on 2021/12/10. 6 | // Copyright © 2021 Hiroyuki Onishi. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import SnapKit 11 | 12 | private let defaultAspectRatio: CGFloat = 1 13 | 14 | @IBDesignable 15 | class LiveThumbnailImageView: NSImageView { 16 | @IBInspectable 17 | var height: Float = 0 { didSet { updateHeight(CGFloat(height)) } } 18 | 19 | override var image: NSImage? { didSet { setImage(image) } } 20 | 21 | private var heightConstraint: Constraint? 22 | private var aspectRatioConstraint: Constraint? 23 | 24 | required init?(coder aDecoder: NSCoder) { 25 | super.init(coder: aDecoder) 26 | } 27 | 28 | override init(frame: CGRect) { 29 | super.init(frame: frame) 30 | } 31 | 32 | override func awakeFromNib() { 33 | configure() 34 | } 35 | } 36 | 37 | private extension LiveThumbnailImageView { 38 | func configure() { 39 | updateHeight(CGFloat(height)) 40 | updateAspectRatio(defaultAspectRatio) 41 | } 42 | 43 | func updateHeight(_ height: CGFloat) { 44 | heightConstraint?.deactivate() 45 | snp.makeConstraints { make in 46 | heightConstraint = make 47 | .height 48 | .equalTo(height) 49 | .constraint 50 | } 51 | } 52 | 53 | func updateAspectRatio(_ ratio: CGFloat) { 54 | aspectRatioConstraint?.deactivate() 55 | snp.makeConstraints { make in 56 | aspectRatioConstraint = make 57 | .height 58 | .equalTo(snp.width) 59 | .multipliedBy(ratio) 60 | .constraint 61 | } 62 | } 63 | 64 | func setImage(_ image: NSImage?) { 65 | super.image = image 66 | let aspectRatio: CGFloat = { 67 | guard let image = image else { return defaultAspectRatio } 68 | return image.size.height / image.size.width 69 | }() 70 | updateAspectRatio(aspectRatio) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Hakumai/Managers/AuthManager/TokenStore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TokenStore.swift 3 | // Hakumai 4 | // 5 | // Created by Hiroyuki Onishi on 2021/05/27. 6 | // Copyright © 2021 Hiroyuki Onishi. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SAMKeychain 11 | 12 | protocol TokenStoreProtocol { 13 | func saveToken(_ storedToken: StoredToken) 14 | func loadToken() -> StoredToken? 15 | func clearToken() 16 | } 17 | 18 | struct StoredToken: Codable { 19 | let accessToken: String 20 | let tokenType: String 21 | let expiresIn: Int 22 | let scope: String 23 | let refreshToken: String 24 | let idToken: String? 25 | } 26 | 27 | private let keychainAccountName = "token" 28 | 29 | final class TokenStore: TokenStoreProtocol {} 30 | 31 | extension TokenStore { 32 | func saveToken(_ storedToken: StoredToken) { 33 | guard let encoded = try? JSONEncoder().encode(storedToken) else { return } 34 | let jsonString = String(decoding: encoded, as: UTF8.self) 35 | log.debug(jsonString) 36 | let success = SAMKeychain.setPassword( 37 | jsonString, 38 | forService: TokenStore.keychainServiceName, 39 | account: keychainAccountName 40 | ) 41 | if !success { 42 | log.error("Failed to save the token to keychain.") 43 | } 44 | } 45 | 46 | func loadToken() -> StoredToken? { 47 | guard let jsonString = SAMKeychain.password( 48 | forService: TokenStore.keychainServiceName, 49 | account: keychainAccountName), 50 | let data = jsonString.data(using: .utf8) else { return nil } 51 | return try? JSONDecoder().decode(StoredToken.self, from: data) 52 | } 53 | 54 | func clearToken() { 55 | SAMKeychain.deletePassword( 56 | forService: TokenStore.keychainServiceName, 57 | account: keychainAccountName 58 | ) 59 | } 60 | } 61 | 62 | private extension TokenStore { 63 | static var keychainServiceName: String = { 64 | (Bundle.main.infoDictionary?["CFBundleIdentifier"] as? String ?? "") + ".token" 65 | }() 66 | } 67 | -------------------------------------------------------------------------------- /HakumaiTests/Models/LiveThumbnailManagerTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LiveThumbnailManagerTest.swift 3 | // HakumaiTests 4 | // 5 | // Created by Hiroyuki Onishi on 2021/12/04. 6 | // Copyright © 2021 Hiroyuki Onishi. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Hakumai 11 | 12 | class LiveThumbnailManagerTest: XCTestCase { 13 | override func setUpWithError() throws {} 14 | override func tearDownWithError() throws {} 15 | 16 | func testExtractThumbnailUrl() throws { 17 | let html = "live_page.html".resourceFileToString() 18 | let manager = LiveThumbnailManager() 19 | 20 | let result = manager.exposedExtractThumbnailUrl(from: html) 21 | let expect = "https://ssth.dmc.nico/thumbnail/20211204/22/00/nicolive-production-pg34220272517733/nicolive-production-pg34220272517733_800_450.jpg?t=1638627321907" 22 | XCTAssert(result?.absoluteString == expect) 23 | } 24 | 25 | // swiftlint:disable force_unwrapping 26 | func testConstructThumbnailUrl() throws { 27 | let manager = LiveThumbnailManager() 28 | var originalUrl: URL 29 | var result: URL? 30 | var expect: String? 31 | 32 | originalUrl = URL(string: "https://ssth.dmc.nico/thumbnail/20211205/11/15/nicolive-production-pg21091510649445/nicolive-production-pg21091510649445_800_450.jpg?t=1638678178738")! 33 | // 2021/12/05 01:02:03 34 | let date = Date.init(timeIntervalSince1970: 1638633723) 35 | result = manager.exposedConstructThumbnailUrl(from: originalUrl, for: date) 36 | // expect = "https://ssth.dmc.nico/thumbnail/20211205/11/15/nicolive-production-pg21091510649445/nicolive-production-pg21091510649445_800_450.jpg?t=1638633723000" 37 | expect = originalUrl.absoluteString 38 | XCTAssert(result?.absoluteString == expect) 39 | 40 | originalUrl = URL(string: "https://secure-dcdn.cdn.nimg.jp/nicoaccount/usericon/defaults/blank.jpg")! 41 | result = manager.exposedConstructThumbnailUrl(from: originalUrl, for: Date()) 42 | // expect = nil 43 | expect = originalUrl.absoluteString 44 | XCTAssert(result?.absoluteString == expect) 45 | } 46 | // swiftlint:enable force_unwrapping 47 | } 48 | -------------------------------------------------------------------------------- /Hakumai/Controllers/BrowserHelper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BrowserHelper.swift 3 | // Hakumai 4 | // 5 | // Created by Hiroyuki Onishi on 1/20/15. 6 | // Copyright (c) 2015 Hiroyuki Onishi. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | private let appNameChrome = "Google Chrome" 12 | private let appNameSafari = "Safari" 13 | 14 | private let chromeScript = """ 15 | if application "\(appNameChrome)" is running then 16 | tell application "\(appNameChrome)" to get URL of active tab of front window as string 17 | end if 18 | """ 19 | 20 | private let safariScript = """ 21 | if application "\(appNameSafari)" is running then 22 | tell application "\(appNameSafari)" to get URL of current tab of front window as string 23 | end if 24 | """ 25 | 26 | final class BrowserHelper { 27 | enum BrowserType { 28 | case chrome 29 | case safari 30 | case firefox 31 | } 32 | 33 | // http://stackoverflow.com/a/6111592 34 | static func extractUrl(fromBrowser browserType: BrowserType) -> String? { 35 | let source = { () -> String in 36 | switch browserType { 37 | case .chrome: return chromeScript 38 | case .safari: return safariScript 39 | default: return "" 40 | } 41 | }() 42 | let script = NSAppleScript(source: source) 43 | var scriptError: NSDictionary? 44 | let descriptor = script?.executeAndReturnError(&scriptError) 45 | 46 | guard scriptError == nil else { 47 | log.error(scriptError) 48 | return nil 49 | } 50 | 51 | var result: String? 52 | if let unicode = descriptor?.coerce(toDescriptorType: UInt32(typeUnicodeText)) { 53 | let data = unicode.data 54 | result = NSString(characters: (data as NSData).bytes.assumingMemoryBound(to: unichar.self), length: (data.count / MemoryLayout.size)) as String 55 | } 56 | return result 57 | } 58 | } 59 | 60 | extension BrowserInUseType { 61 | var toBrowserHelperBrowserType: BrowserHelper.BrowserType { 62 | return { 63 | switch self { 64 | case .chrome: return .chrome 65 | case .safari: return .safari 66 | } 67 | }() 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Hakumai/Extensions/NSColor+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSColor+Extensions.swift 3 | // Hakumai 4 | // 5 | // Created by Hiroyuki Onishi on 2021/12/28. 6 | // Copyright © 2021 Hiroyuki Onishi. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Cocoa 11 | 12 | extension NSColor { 13 | // based on https://github.com/yeahdongcn/UIColor-Hex-Swift 14 | convenience init(hex: String) { 15 | guard hex.hasPrefix("#") else { 16 | fatalError("invalid rgb string, missing '#' as prefix") 17 | } 18 | 19 | var red: CGFloat = 0.0 20 | var green: CGFloat = 0.0 21 | var blue: CGFloat = 0.0 22 | var alpha: CGFloat = 1.0 23 | 24 | let index = hex.index(hex.startIndex, offsetBy: 1) 25 | let _hex = String(hex[index...]) 26 | let scanner = Scanner(string: _hex) 27 | var hexValue: CUnsignedLongLong = 0 28 | 29 | guard scanner.scanHexInt64(&hexValue) else { 30 | fatalError("scan hex error") 31 | } 32 | 33 | if _hex.count == 6 { 34 | red = CGFloat((hexValue & 0xFF0000) >> 16) / 255.0 35 | green = CGFloat((hexValue & 0x00FF00) >> 8) / 255.0 36 | blue = CGFloat(hexValue & 0x0000FF) / 255.0 37 | } else if _hex.count == 8 { 38 | red = CGFloat((hexValue & 0xFF000000) >> 24) / 255.0 39 | green = CGFloat((hexValue & 0x00FF0000) >> 16) / 255.0 40 | blue = CGFloat((hexValue & 0x0000FF00) >> 8) / 255.0 41 | alpha = CGFloat(hexValue & 0x000000FF) / 255.0 42 | } else { 43 | fatalError("invalid rgb string, length should be 7 or 9") 44 | } 45 | 46 | self.init(red: red, green: green, blue: blue, alpha: alpha) 47 | } 48 | 49 | // https://stackoverflow.com/a/39431678/13220031 50 | var hex: String { 51 | guard let rgbColor = usingColorSpace(.sRGB) else { 52 | return "#FFFFFF" 53 | } 54 | let red = Int(round(rgbColor.redComponent * 0xFF)) 55 | let green = Int(round(rgbColor.greenComponent * 0xFF)) 56 | let blue = Int(round(rgbColor.blueComponent * 0xFF)) 57 | let hexString = NSString(format: "#%02X%02X%02X", red, green, blue) 58 | return hexString as String 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Hakumai/Models/NicoManager/NicoOAuthApiModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NicoOAuthApiModel.swift 3 | // Hakumai 4 | // 5 | // Created by Hiroyuki Onishi on 2021/06/06. 6 | // Copyright © 2021 Hiroyuki Onishi. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct MetaResponse: Codable { 12 | let status: Int 13 | let errorCode: String? 14 | } 15 | 16 | struct WatchProgramsResponse: Codable { 17 | struct Data: Codable { 18 | let program: Program 19 | let programProvider: ProgramProvider 20 | } 21 | 22 | enum ProgramStatus: String, Codable { 23 | // https://github.com/niconamaworkshop/api/blob/master/oauth/watch/_program.md 24 | case beforeRelease = "BEFORE_RELEASE" 25 | case released = "RELEASED" 26 | case onAir = "ON_AIR" 27 | case ended = "ENDED" 28 | } 29 | 30 | struct Schedule: Codable { 31 | let beginTime: Date 32 | let endTime: Date 33 | let openTime: Date 34 | let scheduledEndTime: Date 35 | let status: ProgramStatus? 36 | let vposBaseTime: Date 37 | } 38 | 39 | struct Program: Codable { 40 | let title: String 41 | let description: String 42 | let schedule: Schedule 43 | } 44 | 45 | // https://github.com/niconamaworkshop/api/blob/master/oauth/watch/_programProvider.md 46 | struct ProgramProvider: Codable { 47 | let name: String 48 | let profileUrl: URL 49 | let programProviderId: String? 50 | let type: String 51 | let userLevel: Int? 52 | let icons: Icons? 53 | 54 | // swiftlint:disable nesting 55 | struct Icons: Codable { 56 | let uri150x150: URL 57 | let uri50x50: URL 58 | } 59 | // swiftlint:enable nesting 60 | } 61 | 62 | let meta: MetaResponse 63 | let data: Data 64 | } 65 | 66 | struct UserInfoResponse: Codable { 67 | let sub: String 68 | let nickname: String 69 | let profile: URL 70 | let picture: URL 71 | let gender: String? 72 | let zoneinfo: String? 73 | let updatedAt: Int 74 | } 75 | 76 | struct WsEndpointResponse: Codable { 77 | struct Data: Codable { 78 | let url: URL 79 | } 80 | 81 | let meta: MetaResponse 82 | let data: Data 83 | } 84 | -------------------------------------------------------------------------------- /Hakumai/Extensions/NSScrollView+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSScrollView+Extensions.swift 3 | // Hakumai 4 | // 5 | // Created by Hiroyuki Onishi on 2021/05/29. 6 | // Copyright © 2021 Hiroyuki Onishi. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Cocoa 11 | 12 | private let reachAllowance: CGFloat = 16 13 | 14 | extension NSScrollView { 15 | var isReachedToTop: Bool { 16 | let offsetTopY = contentView.documentVisibleRect.origin.y + contentView.contentInsets.top 17 | // log.debug(offsetTopY) 18 | return offsetTopY - reachAllowance < 0 19 | } 20 | 21 | var isReachedToBottom: Bool { 22 | let viewRect = contentView.documentRect 23 | let visibleRect = contentView.documentVisibleRect 24 | // log.debug("\(viewRect)-\(visibleRect)") 25 | 26 | let bottomY = viewRect.size.height 27 | let offsetBottomY = visibleRect.origin.y + visibleRect.size.height 28 | 29 | return bottomY <= (offsetBottomY + reachAllowance) 30 | } 31 | 32 | func scrollToTop() { 33 | contentView.setBoundsOrigin(NSPoint(x: _x, y: _minY)) 34 | flashScrollers() 35 | } 36 | 37 | func scrollToBottom() { 38 | contentView.setBoundsOrigin(NSPoint(x: _x, y: _maxY)) 39 | flashScrollers() 40 | } 41 | 42 | func scrollUp() { 43 | let y = max( 44 | contentView.documentVisibleRect.origin.y 45 | - contentView.documentVisibleRect.size.height 46 | + contentView.contentInsets.top, 47 | _minY) 48 | contentView.setBoundsOrigin(NSPoint(x: _x, y: y)) 49 | flashScrollers() 50 | } 51 | 52 | func scrollDown() { 53 | let y = min( 54 | contentView.documentVisibleRect.origin.y 55 | + contentView.documentVisibleRect.size.height 56 | - contentView.contentInsets.top, 57 | _maxY) 58 | contentView.setBoundsOrigin(NSPoint(x: _x, y: y)) 59 | flashScrollers() 60 | } 61 | } 62 | 63 | private extension NSScrollView { 64 | var _x: CGFloat { contentView.documentVisibleRect.origin.x } 65 | var _minY: CGFloat { -contentView.contentInsets.top } 66 | var _maxY: CGFloat { contentView.documentRect.size.height - contentView.documentVisibleRect.size.height } 67 | } 68 | -------------------------------------------------------------------------------- /Hakumai/Controllers/MainWindowController/HandleNameAddViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HandleNameAddViewController.swift 3 | // Hakumai 4 | // 5 | // Created by Hiroyuki Onishi on 1/4/15. 6 | // Copyright (c) 2015 Hiroyuki Onishi. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import AppKit 11 | 12 | final class HandleNameAddViewController: NSViewController { 13 | // MARK: - Properties 14 | @IBOutlet private weak var titleLabel: NSTextField! 15 | @IBOutlet private weak var cancelButton: NSButton! 16 | @IBOutlet private weak var setButton: NSButton! 17 | 18 | // this property contains handle name value, and is also used binding between text filed and add button. 19 | // http://stackoverflow.com/a/24017991 20 | // and use `dynamic` to make binding work properly. see details at the following link 21 | // - https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/CocoaBindings/Concepts/Troubleshooting.html 22 | // - Changing the value in the user interface programmatically is not reflected in the model 23 | // - Changing the value of a model property programmatically is not reflected in the user interface 24 | // - http://stackoverflow.com/a/26564912 25 | // also use NSString instead of String, cause .length property is used in button's enabled binding. 26 | @objc dynamic var handleName: NSString = "" 27 | 28 | var completion: ((_ cancelled: Bool, _ handleName: String?) -> Void)? 29 | 30 | // MARK: - Object Lifecycle 31 | deinit { log.debug("") } 32 | } 33 | 34 | extension HandleNameAddViewController { 35 | override func viewDidLoad() { 36 | super.viewDidLoad() 37 | configureView() 38 | } 39 | } 40 | 41 | // MARK: - Internal Functions 42 | extension HandleNameAddViewController { 43 | @IBAction func addHandleName(_ sender: AnyObject) { 44 | guard 0 < handleName.length else { return } 45 | completion?(false, handleName as String) 46 | } 47 | 48 | @IBAction func cancelToAdd(_ sender: AnyObject) { 49 | completion?(true, nil) 50 | } 51 | } 52 | 53 | private extension HandleNameAddViewController { 54 | func configureView() { 55 | titleLabel.stringValue = "\(L10n.setUpdateHandleName):" 56 | cancelButton.title = L10n.cancel 57 | setButton.title = L10n.set 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Alamofire (5.10.2) 3 | - DGCharts (5.0.0): 4 | - DGCharts/Core (= 5.0.0) 5 | - DGCharts/Core (5.0.0) 6 | - FMDB (2.7.5): 7 | - FMDB/standard (= 2.7.5) 8 | - FMDB/standard (2.7.5) 9 | - Kanna (5.2.7) 10 | - Kingfisher (6.3.1) 11 | - ObjcExceptionBridging (1.0.1): 12 | - ObjcExceptionBridging/ObjcExceptionBridging (= 1.0.1) 13 | - ObjcExceptionBridging/ObjcExceptionBridging (1.0.1) 14 | - SAMKeychain (1.5.3) 15 | - SnapKit (5.6.0) 16 | - Sparkle (2.1.0) 17 | - Starscream (4.0.6) 18 | - SwiftGen (6.5.1) 19 | - SwiftLint (0.55.1) 20 | - SwiftProtobuf (1.27.1) 21 | - XCGLogger (7.0.1): 22 | - XCGLogger/Core (= 7.0.1) 23 | - XCGLogger/Core (7.0.1): 24 | - ObjcExceptionBridging 25 | 26 | DEPENDENCIES: 27 | - Alamofire (~> 5.10.2) 28 | - DGCharts (~> 5.0.0) 29 | - FMDB (~> 2.7.5) 30 | - Kanna (~> 5.2.7) 31 | - Kingfisher (~> 6.3.1) 32 | - SAMKeychain (~> 1.5.3) 33 | - SnapKit (~> 5.6.0) 34 | - Sparkle (~> 2.1.0) 35 | - Starscream (~> 4.0.6) 36 | - SwiftGen (~> 6.5.1) 37 | - SwiftLint (~> 0.55.1) 38 | - SwiftProtobuf (~> 1.0) 39 | - XCGLogger (~> 7.0.1) 40 | 41 | SPEC REPOS: 42 | trunk: 43 | - Alamofire 44 | - DGCharts 45 | - FMDB 46 | - Kanna 47 | - Kingfisher 48 | - ObjcExceptionBridging 49 | - SAMKeychain 50 | - SnapKit 51 | - Sparkle 52 | - Starscream 53 | - SwiftGen 54 | - SwiftLint 55 | - SwiftProtobuf 56 | - XCGLogger 57 | 58 | SPEC CHECKSUMS: 59 | Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496 60 | DGCharts: 6e0cf2644e8f81ad13f04caa8bc18502870a1c54 61 | FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a 62 | Kanna: 01cfbddc127f5ff0963692f285fcbc8a9d62d234 63 | Kingfisher: 016c8b653a35add51dd34a3aba36b580041acc74 64 | ObjcExceptionBridging: c30e00eb3700467e695faeea30e26e18bd445001 65 | SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c 66 | SnapKit: e01d52ebb8ddbc333eefe2132acf85c8227d9c25 67 | Sparkle: 7f5f6d4328458515e23272f4138e610c1fa3e32d 68 | Starscream: fb2c4510bebf908c62bd383bcf05e673720e91fd 69 | SwiftGen: a6d22010845f08fe18fbdf3a07a8e380fd22e0ea 70 | SwiftLint: 3fe909719babe5537c552ee8181c0031392be933 71 | SwiftProtobuf: b109bd17979d7993a84da14b1e1fdd8b0ded934a 72 | XCGLogger: 1943831ef907df55108b0b18657953f868de973b 73 | 74 | PODFILE CHECKSUM: 57ab28ced641ad2a8f4aed30f6b09eb66b0a413f 75 | 76 | COCOAPODS: 1.16.2 77 | -------------------------------------------------------------------------------- /Hakumai/Controllers/PreferenceWindowController/MuteViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MuteViewController.swift 3 | // Hakumai 4 | // 5 | // Created by Hiroyuki Onishi on 12/11/14. 6 | // Copyright (c) 2014 Hiroyuki Onishi. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import AppKit 11 | 12 | final class MuteViewController: NSViewController { 13 | // MARK: - Properties 14 | static let shared = MuteViewController.make() 15 | 16 | @IBOutlet private weak var enableMuteUserIdButton: NSButton! 17 | @IBOutlet private weak var enableMuteWordButton: NSButton! 18 | @IBOutlet private var muteUserIdsArrayController: NSArrayController! 19 | @IBOutlet private var muteWordsArrayController: NSArrayController! 20 | 21 | // MARK: - Object Lifecycle 22 | static func make() -> MuteViewController { 23 | return StoryboardScene.PreferenceWindowController.muteViewController.instantiate() 24 | } 25 | } 26 | 27 | extension MuteViewController { 28 | override func viewDidLoad() { 29 | super.viewDidLoad() 30 | configureView() 31 | } 32 | } 33 | 34 | extension MuteViewController { 35 | // MARK: - Button Handlers 36 | @IBAction func addMuteUserId(_ sender: AnyObject) { 37 | addMute { muteStringValue in 38 | self.muteUserIdsArrayController.addObject(["UserId": muteStringValue]) 39 | } 40 | } 41 | 42 | @IBAction func addMuteWord(_ sender: AnyObject) { 43 | addMute { muteStringValue in 44 | self.muteWordsArrayController.addObject(["Word": muteStringValue]) 45 | } 46 | } 47 | } 48 | 49 | private extension MuteViewController { 50 | func configureView() { 51 | enableMuteUserIdButton.title = L10n.enableMuteUserIds 52 | enableMuteWordButton.title = L10n.enableMuteWords 53 | } 54 | 55 | func addMute(completion: @escaping (String) -> Void) { 56 | let muteAddViewController = 57 | StoryboardScene.PreferenceWindowController.muteAddViewController.instantiate() 58 | muteAddViewController.completion = { (cancelled, muteStringValue) in 59 | if !cancelled, let muteStringValue = muteStringValue { 60 | completion(muteStringValue) 61 | } 62 | self.dismiss(muteAddViewController) 63 | // TODO: deinit in muteAddViewController is not called after this completion 64 | } 65 | presentAsSheet(muteAddViewController) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /HakumaiTests/Models/HandleNameManagerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HandleNameManagerTests.swift 3 | // Hakumai 4 | // 5 | // Created by Hiroyuki Onishi on 1/4/15. 6 | // Copyright (c) 2015 Hiroyuki Onishi. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import XCTest 11 | @testable import Hakumai 12 | 13 | final class HandleNameManagerTests: XCTestCase { 14 | override func setUp() { 15 | super.setUp() 16 | } 17 | 18 | override func tearDown() { 19 | super.tearDown() 20 | } 21 | 22 | func testExtractHandleName() { 23 | // full-width at mark 24 | checkExtractHandleName("わこ@あいうえお", expected: "あいうえお") 25 | checkExtractHandleName("@あいうえお", expected: "あいうえお") 26 | 27 | // normal at mark 28 | checkExtractHandleName("わこ@あいうえお", expected: "あいうえお") 29 | checkExtractHandleName("@あいうえお", expected: "あいうえお") 30 | 31 | // has space 32 | checkExtractHandleName("わこ@ あいうえお", expected: "あいうえお") 33 | checkExtractHandleName("わこ@あいうえお ", expected: "あいうえお") 34 | checkExtractHandleName("わこ@ あいうえお ", expected: "あいうえお") 35 | checkExtractHandleName("わこ@ あいうえお", expected: "あいうえお") 36 | checkExtractHandleName("わこ@あいうえお ", expected: "あいうえお") 37 | checkExtractHandleName("わこ@ あいうえお ", expected: "あいうえお") 38 | 39 | // user comment that notifies live remaining minutes 40 | checkExtractHandleName("@5", expected: nil) 41 | checkExtractHandleName("@5", expected: nil) 42 | checkExtractHandleName("@10", expected: nil) 43 | checkExtractHandleName("@10", expected: nil) 44 | checkExtractHandleName("@96猫", expected: "96猫") 45 | checkExtractHandleName("@96猫", expected: "96猫") 46 | 47 | // mail address 48 | checkExtractHandleName("ご連絡はmail@example.comまで", expected: nil) 49 | } 50 | 51 | func checkExtractHandleName(_ comment: String, expected: String?) { 52 | XCTAssert(HandleNameManager.shared.extractHandleName(from: comment) == expected, "") 53 | } 54 | 55 | func testUpsertThenSelectHandleName() { 56 | let communityId = "co" + String(Int.random(in: 0...99)) 57 | let userId = String(Int.random(in: 0...99)) 58 | let handleName = "山田" 59 | 60 | HandleNameManager.shared.upsert(handleName: handleName, for: userId, in: communityId) 61 | 62 | let resolved = HandleNameManager.shared.selectHandleName(for: userId, in: communityId) 63 | XCTAssert(resolved == handleName, "") 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Hakumai/Controllers/UIHelper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIHelper.swift 3 | // Hakumai 4 | // 5 | // Created by Hiroyuki Onishi on 11/25/14. 6 | // Copyright (c) 2014 Hiroyuki Onishi. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import AppKit 11 | 12 | final class UIHelper { 13 | // MARK: - Main Window Colors 14 | static func systemMessageBgColor() -> NSColor { 15 | return NSColor(hex: "#808080") 16 | } 17 | 18 | static func debugMessageBgColor() -> NSColor { 19 | return NSColor(hex: "#101010") 20 | } 21 | 22 | static func greenScoreColor() -> NSColor { 23 | return NSColor(hex: "#0EA50B") 24 | } 25 | 26 | static func roomColor(roomPosition: RoomPosition) -> NSColor { 27 | switch roomPosition { 28 | case .arena: 29 | return NSColor(hex: "#3C49FF") 30 | case .storeA, .storeB, .storeC, .storeD, .storeE, .storeF, .storeG, .storeH, .storeI, .storeJ: 31 | return NSColor(hex: "#CB4C15") 32 | } 33 | } 34 | 35 | static func cellViewAdFlashColor() -> NSColor { 36 | return colorIf(light: "#FFBD2F", dark: "#E09900") 37 | } 38 | 39 | static func cellViewGiftFlashColor() -> NSColor { 40 | return colorIf(light: "#E5444F", dark: "#D01C24") 41 | } 42 | 43 | static func casterCommentColor() -> NSColor { 44 | return colorIf(light: "#D22E1B", dark: "#FF8170") 45 | } 46 | 47 | // MARK: - Font Attributes 48 | static func commentAttributes( 49 | fontSize: CGFloat = CGFloat(kDefaultFontSize), 50 | isBold: Bool = false, 51 | isRed: Bool = false 52 | ) -> [NSAttributedString.Key: Any] { 53 | return [ 54 | .font: isBold ? NSFont.boldSystemFont(ofSize: fontSize) : NSFont.systemFont(ofSize: fontSize), 55 | .foregroundColor: isRed ? casterCommentColor() : NSColor.labelColor, 56 | .paragraphStyle: NSParagraphStyle.default 57 | ] 58 | } 59 | } 60 | 61 | private extension UIHelper { 62 | static func colorIf(light: String, dark: String) -> NSColor { 63 | if #available(macOS 10.14, *) { 64 | return NSApplication.shared.isDarkMode ? NSColor(hex: dark) : NSColor(hex: light) 65 | } else { 66 | return NSColor(hex: light) 67 | } 68 | } 69 | } 70 | 71 | private extension NSApplication { 72 | var isDarkMode: Bool { 73 | if #available(OSX 10.14, *) { 74 | return effectiveAppearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua 75 | } 76 | return false 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /AGENTS.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Agents 4 | --- 5 | 6 | # Agents 7 | 8 | ## Overview 9 | 10 | Hakumai relies on specialized agents to automate interactions with external services and streamline workflows for broadcasters and moderators. This document outlines the expected behavior, responsibilities, and operational guidelines for each agent that participates in the Hakumai ecosystem. 11 | 12 | ## Agent Roles 13 | 14 | - **Niconico Integration Agent** — synchronizes live broadcast metadata, room status, and available comment streams. 15 | - **Comment Processing Agent** — filters, ranks, and forwards incoming messages to the desktop client and optional speech synthesis pipelines. 16 | - **Notification Agent** — dispatches alerts for critical events (connection issues, moderator actions, donation triggers) to the appropriate user channels. 17 | 18 | Use these role descriptions as a baseline when designing new automation flows or refining existing ones. 19 | 20 | ## Communication Guidelines 21 | 22 | - All interactions with user-facing agents must be conducted in Japanese to match the expectations of Hakumai broadcasters and viewers. 23 | - Logs and telemetry generated for internal diagnostics may remain in English for consistency with the codebase. 24 | - When integrating third-party APIs, prefer English for protocol-level messages but provide localized summaries to users. 25 | 26 | ## Operational Workflow 27 | 28 | 1. Monitor platform events for triggers relevant to the assigned role. 29 | 2. Validate incoming data, enrich it if necessary, and normalize formats before distribution. 30 | 3. Deliver responses or actions through the designated channels while adhering to the communication guidelines above. 31 | 4. Record outcomes and status updates for later auditing and troubleshooting. 32 | 33 | ## Reliability and Monitoring 34 | 35 | - Implement retry logic with exponential backoff for recoverable failures. 36 | - Surface actionable error messages in Japanese when user intervention is required. 37 | - Provide structured metrics (latency, throughput, failure rates) to the central monitoring dashboard. 38 | 39 | ## Security Considerations 40 | 41 | - Store credentials using the Hakumai secure storage facilities; never embed secrets in agent code or logs. 42 | - Restrict agent permissions to the minimum scope needed for each task. 43 | - Audit third-party dependencies regularly and track upstream security advisories. 44 | 45 | ## Next Steps 46 | 47 | Expand each section with concrete implementation details, configuration examples, and escalation procedures as the agent architecture matures. 48 | -------------------------------------------------------------------------------- /Hakumai/Models/NicoManager/NicoWebSocketModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NicoWebSocketModel.swift 3 | // Hakumai 4 | // 5 | // Created by Hiroyuki Onishi on 2021/05/16. 6 | // Copyright © 2021 Hiroyuki Onishi. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: - WebSocket (Watch, Generic Model) 12 | enum WebSocketDataType: String, Codable { 13 | case ping, seat, messageServer, statistics, disconnect, reconnect 14 | } 15 | 16 | struct WebSocketData: Codable { 17 | let type: WebSocketDataType 18 | } 19 | 20 | // MARK: - WebSocket (Watch, Specific Model) 21 | struct WebSocketPingData: Codable { 22 | let type: WebSocketDataType 23 | } 24 | 25 | struct WebSocketSeatData: Codable { 26 | struct Data: Codable { 27 | let keepIntervalSec: Int 28 | } 29 | 30 | let type: WebSocketDataType 31 | let data: Data 32 | } 33 | 34 | // {"type":"messageServer","data":{"viewUri":"https://mpn.live.nicovideo.jp/api/view/v4/BBzh6D87sTyygFaji0QUuxYWeJbYqgeRcbK7DumuGq4bnH4mCSWhNCjr_Y2D6X6ksyGVx9swrDbd","vposBaseTime":"2024-08-05T17:00:00+09:00","hashedUserId":"a:U5xlhzXQVY6YzMW1"}} 35 | struct WebSocketMessageServerData: Codable { 36 | struct Data: Codable { 37 | let viewUri: String 38 | let vposBaseTime: String 39 | let hashedUserId: String 40 | } 41 | 42 | let type: WebSocketDataType 43 | let data: Data 44 | } 45 | 46 | struct WebSocketStatisticsData: Codable { 47 | struct Data: Codable { 48 | let viewers: Int 49 | let comments: Int 50 | let adPoints: Int? 51 | let giftPoints: Int? 52 | } 53 | 54 | let type: WebSocketDataType 55 | let data: Data 56 | } 57 | 58 | struct WebSocketDisconnectData: Codable { 59 | let type: WebSocketDataType 60 | } 61 | 62 | struct WebSocketReconnectData: Codable { 63 | struct Data: Codable { 64 | let audienceToken: String 65 | let waitTimeSec: Int 66 | } 67 | 68 | let type: WebSocketDataType 69 | } 70 | 71 | // MARK: - WebSocket (Message) 72 | struct WebSocketChatData: Codable { 73 | struct WebSocketChat: Codable { 74 | let thread: String 75 | let no: Int 76 | let vpos: Int 77 | let date: Int 78 | let dateUsec: Int 79 | let mail: String? 80 | let userId: String 81 | let premium: Int? 82 | let anonymity: Int? 83 | let content: String 84 | } 85 | 86 | let chat: WebSocketChat 87 | } 88 | 89 | struct WebSocketPingContentData: Codable { 90 | struct Ping: Codable { 91 | let content: String 92 | } 93 | 94 | let ping: Ping 95 | } 96 | -------------------------------------------------------------------------------- /Hakumai/Models/Application/Enum.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Enum.swift 3 | // Hakumai 4 | // 5 | // Created by Hiroyuki Onishi on 11/19/14. 6 | // Copyright (c) 2014 Hiroyuki Onishi. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | enum BrowserType { 12 | case chrome, safari 13 | } 14 | 15 | enum RoomPosition: Int, CaseIterable { 16 | case arena = 0 17 | case storeA, storeB, storeC, storeD, storeE, storeF, storeG, storeH, storeI, storeJ 18 | 19 | // swiftlint:disable cyclomatic_complexity 20 | func label() -> String { 21 | switch self { 22 | case .arena: return "アリーナ" 23 | case .storeA: return "Store1" 24 | case .storeB: return "Store2" 25 | case .storeC: return "Store3" 26 | case .storeD: return "Store4" 27 | case .storeE: return "Store5" 28 | case .storeF: return "Store6" 29 | case .storeG: return "Store7" 30 | case .storeH: return "Store8" 31 | case .storeI: return "Store9" 32 | case .storeJ: return "Store10" 33 | } 34 | } 35 | // swiftlint:enable cyclomatic_complexity 36 | 37 | // swiftlint:disable cyclomatic_complexity 38 | func shortLabel() -> String { 39 | switch self { 40 | case .arena: return "ア" 41 | case .storeA: return "1" 42 | case .storeB: return "2" 43 | case .storeC: return "3" 44 | case .storeD: return "4" 45 | case .storeE: return "5" 46 | case .storeF: return "6" 47 | case .storeG: return "7" 48 | case .storeH: return "8" 49 | case .storeI: return "9" 50 | case .storeJ: return "10" 51 | } 52 | } 53 | // swiftlint:enable cyclomatic_complexity 54 | } 55 | 56 | enum Premium: Int, CustomStringConvertible { 57 | case ippan = 0 58 | case premium = 1 59 | case system = 2 // '/disconnect' 60 | case caster = 3 61 | case `operator` = 6 62 | case bsp = 7 63 | case ippanTransparent = 24 64 | 65 | var description: String { return "\(rawValue)(\(label()))" } 66 | 67 | func label() -> String { 68 | switch self { 69 | case .ippan: return "一般" 70 | case .premium: return "プレミアム" 71 | case .system: return "システム" 72 | case .caster: return "放送主" 73 | case .operator: return "運営" 74 | case .bsp: return "BSP" 75 | case .ippanTransparent: return "一般 (透明)" 76 | } 77 | } 78 | 79 | var isSystem: Bool { [.system, .caster, .operator].contains(self) } 80 | var isUser: Bool { [.ippan, .premium, .ippanTransparent].contains(self) } 81 | } 82 | -------------------------------------------------------------------------------- /Hakumai/Managers/SpeechManager/AudioCacher.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AudioCacher.swift 3 | // Hakumai 4 | // 5 | // Created by Hiroyuki Onishi on 2022/02/11. 6 | // Copyright © 2022 Hiroyuki Onishi. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol AudioCacherType { 12 | func get(speedScale: Float, volumeScale: Float, speaker: Int, text: String) -> Data? 13 | func set(speedScale: Float, volumeScale: Float, speaker: Int, text: String, data: Data) 14 | } 15 | 16 | private let cacheCountLimit = 100 17 | 18 | final class AudioCacher: NSObject, AudioCacherType { 19 | static let shared = AudioCacher() 20 | 21 | private let cache = NSCache() 22 | 23 | override init() { 24 | super.init() 25 | cache.delegate = self 26 | cache.countLimit = cacheCountLimit 27 | } 28 | 29 | deinit { log.debug("") } 30 | 31 | func get(speedScale: Float, volumeScale: Float, speaker: Int, text: String) -> Data? { 32 | let key = cacheKey(speedScale, volumeScale, speaker, text) 33 | return cache.object(forKey: key)?.data 34 | } 35 | 36 | func set(speedScale: Float, volumeScale: Float, speaker: Int, text: String, data: Data) { 37 | let key = cacheKey(speedScale, volumeScale, speaker, text) 38 | let object = AudioData( 39 | speedScale: speedScale, 40 | volumeScale: volumeScale, 41 | speaker: speaker, 42 | text: text, 43 | data: data) 44 | cache.setObject(object, forKey: key) 45 | } 46 | } 47 | 48 | extension AudioCacher: NSCacheDelegate { 49 | func cache(_ cache: NSCache, willEvictObject obj: Any) { 50 | log.debug("Audio cache evicted: \(obj)") 51 | } 52 | } 53 | 54 | private extension AudioCacher { 55 | func cacheKey(_ speedScale: Float, _ volumeScale: Float, _ speaker: Int, _ text: String) -> NSString { 56 | return "\(speedScale):\(volumeScale):\(speaker):\(text.hashValue)" as NSString 57 | } 58 | } 59 | 60 | private class AudioData: CustomStringConvertible { 61 | let speedScale: Float 62 | let volumeScale: Float 63 | let speaker: Int 64 | let text: String 65 | let data: Data 66 | 67 | var description: String { "\(speedScale)/\(volumeScale)/\(speaker)/\(text)/\(data)"} 68 | 69 | init(speedScale: Float, volumeScale: Float, speaker: Int, text: String, data: Data) { 70 | self.speedScale = speedScale 71 | self.volumeScale = volumeScale 72 | self.speaker = speaker 73 | self.text = text 74 | self.data = data 75 | } 76 | 77 | deinit { log.debug("") } 78 | } 79 | -------------------------------------------------------------------------------- /Hakumai/Controllers/MainWindowController/TableCellView/IconTableCellView.xib: -------------------------------------------------------------------------------- 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 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /Hakumai/Infrastructures/Keychain/KeychainUtility.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeychainUtility.swift 3 | // Hakumai 4 | // 5 | // Created by Hiroyuki Onishi on 1/6/15. 6 | // Copyright (c) 2015 Hiroyuki Onishi. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SAMKeychain 11 | 12 | final class KeychainUtility { 13 | // MARK: - Public Functions 14 | static func removeAllAccountsInKeychain() { 15 | let serviceName = KeychainUtility.keychainServiceName() 16 | 17 | if let accounts = SAMKeychain.accounts(forService: serviceName) { 18 | for account in accounts { 19 | if let accountName = account[kSAMKeychainAccountKey] as? NSString { 20 | if SAMKeychain.deletePassword(forService: serviceName, account: accountName as String) == true { 21 | log.debug("completed to delete account from keychain:[\(accountName)]") 22 | } else { 23 | log.error("failed to delete account from keychain:[\(accountName)]") 24 | } 25 | } 26 | } 27 | } 28 | } 29 | 30 | static func setAccountToKeychain(mailAddress: String, password: String) { 31 | let serviceName = KeychainUtility.keychainServiceName() 32 | 33 | if SAMKeychain.setPassword(password, forService: serviceName, account: mailAddress) == true { 34 | log.debug("completed to set account into keychain:[\(mailAddress)]") 35 | } else { 36 | log.error("failed to set account into keychain:[\(mailAddress)]") 37 | } 38 | } 39 | 40 | static func accountInKeychain() -> (mailAddress: String, password: String)? { 41 | let serviceName = KeychainUtility.keychainServiceName() 42 | 43 | if let accounts = SAMKeychain.accounts(forService: serviceName) { 44 | guard let accountName = accounts.last?[kSAMKeychainAccountKey] as? String, 45 | let password = SAMKeychain.password(forService: serviceName, account: accountName) else { 46 | return nil 47 | } 48 | log.debug("found account in keychain:[\(accountName)]") 49 | return (accountName, password) 50 | } 51 | 52 | log.debug("found no account in keychain") 53 | return nil 54 | } 55 | } 56 | 57 | private extension KeychainUtility { 58 | static func keychainServiceName() -> String { 59 | var bundleIdentifier = "" 60 | if let bi = Bundle.main.infoDictionary?["CFBundleIdentifier"] as? String { 61 | bundleIdentifier = bi 62 | } 63 | 64 | return bundleIdentifier + ".account" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Hakumai/Controllers/MainWindowController/TableCellView/TimeTableCellView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimeTableCellView.swift 3 | // Hakumai 4 | // 5 | // Created by Hiroyuki Onishi on 11/25/14. 6 | // Copyright (c) 2014 Hiroyuki Onishi. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import AppKit 11 | 12 | private let defaultTimeValue = "-" 13 | 14 | final class TimeTableCellView: NSTableCellView { 15 | @IBOutlet weak var coloredView: ColoredView! 16 | @IBOutlet weak var timeLabel: NSTextField! 17 | 18 | var fontSize: CGFloat? { didSet { set(fontSize: fontSize) } } 19 | } 20 | 21 | extension TimeTableCellView { 22 | func configure(live: Live?, message: Message?) { 23 | coloredView.fillColor = color(message: message) 24 | timeLabel.stringValue = time(live: live, message: message) 25 | } 26 | } 27 | 28 | private extension TimeTableCellView { 29 | func color(message: Message?) -> NSColor { 30 | guard let message = message else { return UIHelper.systemMessageBgColor() } 31 | switch message.content { 32 | case .system: 33 | return UIHelper.systemMessageBgColor() 34 | case .chat(let chat): 35 | return chat.isSystem ? UIHelper.systemMessageBgColor() : UIHelper.greenScoreColor() 36 | case .debug: 37 | return UIHelper.debugMessageBgColor() 38 | } 39 | } 40 | 41 | func time(live: Live?, message: Message?) -> String { 42 | guard let message = message else { return defaultTimeValue } 43 | switch message.content { 44 | case .system, .debug: 45 | return "[\(message.date.toLocalTimeString())]" 46 | case .chat(chat: let chat): 47 | guard let beginDate = live?.beginTime, 48 | let elapsed = chat.date.toElapsedTimeString(from: beginDate) else { 49 | return defaultTimeValue 50 | } 51 | return elapsed 52 | } 53 | } 54 | 55 | func set(fontSize: CGFloat?) { 56 | let size = fontSize ?? CGFloat(kDefaultFontSize) 57 | timeLabel.font = NSFont.systemFont(ofSize: size) 58 | } 59 | } 60 | 61 | private extension Date { 62 | func toElapsedTimeString(from fromDate: Date) -> String? { 63 | let comps = Calendar.current.dateComponents( 64 | [.hour, .minute, .second], from: fromDate, to: self) 65 | guard let h = comps.hour, let m = comps.minute, let s = comps.second else { return nil } 66 | return "\(h):\(m.zeroPadded):\(s.zeroPadded)" 67 | } 68 | 69 | func toLocalTimeString() -> String { 70 | let formatter = DateFormatter() 71 | formatter.dateFormat = "H:mm:ss" 72 | return formatter.string(from: self) 73 | } 74 | } 75 | 76 | private extension Int { 77 | var zeroPadded: String { String(format: "%02d", self) } 78 | } 79 | -------------------------------------------------------------------------------- /Hakumai/Controllers/UserWindowController/UserWindowController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserWindowController.swift 3 | // Hakumai 4 | // 5 | // Created by Hiroyuki Onishi on 12/22/14. 6 | // Copyright (c) 2014 Hiroyuki Onishi. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import AppKit 11 | 12 | protocol UserWindowControllerDelegate: AnyObject { 13 | func userWindowControllerWillClose(_ userWindowController: UserWindowController) 14 | } 15 | 16 | final class UserWindowController: NSWindowController { 17 | // MARK: - Properties 18 | private weak var delegate: UserWindowControllerDelegate? 19 | private(set) var userId: String = "" 20 | 21 | // MARK: - Object Lifecycle 22 | deinit { 23 | log.debug("") 24 | } 25 | 26 | override func windowDidLoad() { 27 | super.windowDidLoad() 28 | window?.isMovableByWindowBackground = true 29 | } 30 | } 31 | 32 | extension UserWindowController { 33 | // swiftlint:disable function_parameter_count 34 | static func make(delegate: UserWindowControllerDelegate?, nicoManager: NicoManagerType, live: Live, messageContainer: MessageContainer, userId: String, handleName: String?) -> UserWindowController { 35 | let wc = StoryboardScene.UserWindowController.userWindowController.instantiate() 36 | wc.delegate = delegate 37 | wc.set( 38 | nicoManager: nicoManager, 39 | live: live, 40 | messageContainer: messageContainer, 41 | userId: userId, 42 | handleName: handleName 43 | ) 44 | return wc 45 | } 46 | // swiftlint:enable function_parameter_count 47 | } 48 | 49 | extension UserWindowController: NSWindowDelegate { 50 | func windowWillClose(_ notification: Notification) { 51 | let window: Any? = notification.object 52 | if window is UserWindow { 53 | delegate?.userWindowControllerWillClose(self) 54 | } 55 | } 56 | } 57 | 58 | // MARK: - Public Functions 59 | extension UserWindowController { 60 | func set(nicoManager: NicoManagerType, live: Live, messageContainer: MessageContainer, userId: String, handleName: String?) { 61 | self.userId = userId 62 | let userName = nicoManager.cachedUserName(for: userId) 63 | window?.title = (handleName ?? userName ?? userId) + " (\(live.title))" 64 | guard let userViewController = contentViewController as? UserViewController else { return } 65 | userViewController.set( 66 | nicoManager: nicoManager, 67 | live: live, 68 | messageContainer: messageContainer, 69 | userId: userId, 70 | handleName: handleName 71 | ) 72 | } 73 | 74 | func reloadMessages() { 75 | guard let userViewController = contentViewController as? UserViewController else { return } 76 | userViewController.reloadMessages() 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | --- 4 | 5 | Niconama Comment Viewer Alternative for macOS. 6 | 7 | 8 | Download 9 | v{{site.binary_version}} / {{site.binary_date}} / 10 | 11 | Release Notes 12 | 13 | ## About Hakumai 14 | 15 | Hakumai は macOS で動作するニコニコ生放送用のコメントビューア (コメビュ) です。 16 | 17 | 18 | 19 | ### Requirements 20 | 21 | * macOS 10.15 Catalina 以降 22 | * VOICEVOX 連携には別途 VOICEVOX ダウンロード 23 | 24 | 25 | 26 | ## Uninstall Hakumai, Completely 27 | 28 | ``` 29 | 1. rm /path/to/Hakumai.app 30 | 2. rm ~/Library/Preferences/com.honishi.Hakumai.plist 31 | 3. rm -r ~/Library/Application\ Support/com.honishi.Hakumai/ 32 | 4. Keychain Access アプリで "com.honishi.Hakumai.token" を削除 33 | ``` 34 | 35 | 36 | 37 | ## Donation 38 | 39 |
40 |
41 |
42 | 🌾 ドネーション 43 | 🌾 ほしい物リスト 🌾 44 |
45 |
46 | 47 | ## Contact 48 | 49 | * @honishi 50 | 51 | 52 | 53 | ## Special Thanks 54 | 55 |
56 | 57 | あああ / イノシシ / うまごん / うんさい / ジャスティス名川 / ジュディア / 田中アオ / 七原くん / 向日葵 / 藤澤タック / もっちゃん 58 | 59 |
-------------------------------------------------------------------------------- /docs/css/syntax.css: -------------------------------------------------------------------------------- 1 | .hll { background-color: #ffffcc } 2 | /*{ background: #f0f3f3; }*/ 3 | .c { color: #999; } /* Comment */ 4 | .err { color: #AA0000; background-color: #FFAAAA } /* Error */ 5 | .k { color: #006699; } /* Keyword */ 6 | .o { color: #555555 } /* Operator */ 7 | .cm { color: #0099FF; font-style: italic } /* Comment.Multiline */ 8 | .cp { color: #009999 } /* Comment.Preproc */ 9 | .c1 { color: #999; } /* Comment.Single */ 10 | .cs { color: #999; } /* Comment.Special */ 11 | .gd { background-color: #FFCCCC; border: 1px solid #CC0000 } /* Generic.Deleted */ 12 | .ge { font-style: italic } /* Generic.Emph */ 13 | .gr { color: #FF0000 } /* Generic.Error */ 14 | .gh { color: #003300; } /* Generic.Heading */ 15 | .gi { background-color: #CCFFCC; border: 1px solid #00CC00 } /* Generic.Inserted */ 16 | .go { color: #AAAAAA } /* Generic.Output */ 17 | .gp { color: #000099; } /* Generic.Prompt */ 18 | .gs { } /* Generic.Strong */ 19 | .gu { color: #003300; } /* Generic.Subheading */ 20 | .gt { color: #99CC66 } /* Generic.Traceback */ 21 | .kc { color: #006699; } /* Keyword.Constant */ 22 | .kd { color: #006699; } /* Keyword.Declaration */ 23 | .kn { color: #006699; } /* Keyword.Namespace */ 24 | .kp { color: #006699 } /* Keyword.Pseudo */ 25 | .kr { color: #006699; } /* Keyword.Reserved */ 26 | .kt { color: #007788; } /* Keyword.Type */ 27 | .m { color: #FF6600 } /* Literal.Number */ 28 | .s { color: #d44950 } /* Literal.String */ 29 | .na { color: #4f9fcf } /* Name.Attribute */ 30 | .nb { color: #336666 } /* Name.Builtin */ 31 | .nc { color: #00AA88; } /* Name.Class */ 32 | .no { color: #336600 } /* Name.Constant */ 33 | .nd { color: #9999FF } /* Name.Decorator */ 34 | .ni { color: #999999; } /* Name.Entity */ 35 | .ne { color: #CC0000; } /* Name.Exception */ 36 | .nf { color: #CC00FF } /* Name.Function */ 37 | .nl { color: #9999FF } /* Name.Label */ 38 | .nn { color: #00CCFF; } /* Name.Namespace */ 39 | .nt { color: #2f6f9f; } /* Name.Tag */ 40 | .nv { color: #003333 } /* Name.Variable */ 41 | .ow { color: #000000; } /* Operator.Word */ 42 | .w { color: #bbbbbb } /* Text.Whitespace */ 43 | .mf { color: #FF6600 } /* Literal.Number.Float */ 44 | .mh { color: #FF6600 } /* Literal.Number.Hex */ 45 | .mi { color: #FF6600 } /* Literal.Number.Integer */ 46 | .mo { color: #FF6600 } /* Literal.Number.Oct */ 47 | .sb { color: #CC3300 } /* Literal.String.Backtick */ 48 | .sc { color: #CC3300 } /* Literal.String.Char */ 49 | .sd { color: #CC3300; font-style: italic } /* Literal.String.Doc */ 50 | .s2 { color: #CC3300 } /* Literal.String.Double */ 51 | .se { color: #CC3300; } /* Literal.String.Escape */ 52 | .sh { color: #CC3300 } /* Literal.String.Heredoc */ 53 | .si { color: #AA0000 } /* Literal.String.Interpol */ 54 | .sx { color: #CC3300 } /* Literal.String.Other */ 55 | .sr { color: #33AAAA } /* Literal.String.Regex */ 56 | .s1 { color: #CC3300 } /* Literal.String.Single */ 57 | .ss { color: #FFCC33 } /* Literal.String.Symbol */ 58 | .bp { color: #336666 } /* Name.Builtin.Pseudo */ 59 | .vc { color: #003333 } /* Name.Variable.Class */ 60 | .vg { color: #003333 } /* Name.Variable.Global */ 61 | .vi { color: #003333 } /* Name.Variable.Instance */ 62 | .il { color: #FF6600 } /* Literal.Number.Integer.Long */ 63 | 64 | .css .o, 65 | .css .o + .nt, 66 | .css .nt + .nt { color: #999; } -------------------------------------------------------------------------------- /docs/css/solo.css: -------------------------------------------------------------------------------- 1 | @import url(//fonts.googleapis.com/css?family=Montserrat:700); 2 | @import url(//fonts.googleapis.com/css?family=Inconsolata:400,700); 3 | 4 | html { 5 | font: 16px/1.5 Inconsolata, sans-serif; 6 | } 7 | 8 | @media (min-width: 30rem) { 9 | html { 10 | font-size: 20px; 11 | } 12 | } 13 | 14 | body { 15 | margin: 2rem 0 5rem; 16 | color: #333; 17 | background-image: url("../image/hakumai.jpg"); 18 | background-size: 20%; 19 | } 20 | 21 | @media (min-width: 30rem) { 22 | body { 23 | margin-top: 5rem; 24 | } 25 | } 26 | 27 | a { 28 | color: #0074d9; /* From http://clrs.cc */ 29 | text-decoration: none; 30 | } 31 | 32 | a:hover, a:focus { 33 | text-decoration: underline; 34 | } 35 | 36 | h1, h2, h3, h4, h5, h6 { 37 | font-family: Montserrat, sans-serif; 38 | margin: 0 0 0.5rem -0.1rem /* align left edge */; 39 | line-height: 1; 40 | color: #111; 41 | text-rendering: optimizeLegibility; 42 | } 43 | 44 | h1 { 45 | font-size: 2.5rem; 46 | margin-bottom: 1rem; 47 | } 48 | 49 | @media (min-width: 30rem) { 50 | h1 { 51 | font-size: 3rem; 52 | margin-bottom: 3rem; 53 | } 54 | } 55 | 56 | h1 a { 57 | color: inherit; 58 | } 59 | 60 | h2 { 61 | margin-top: 2rem; 62 | font-size: 1.25rem; 63 | margin-bottom: 0.75rem; 64 | } 65 | 66 | @media (min-width: 30rem) { 67 | h2 { 68 | margin-top: 2.5rem; 69 | font-size: 1.5rem; 70 | margin-bottom: 1rem; 71 | } 72 | } 73 | 74 | h3, h4, h5, h6 { 75 | margin-top: 1.5rem; 76 | font-size: 1rem; 77 | text-transform: uppercase; 78 | } 79 | 80 | p, ul, ol, dl, table, pre, blockquote { 81 | margin-top: 0; 82 | margin-bottom: 1rem; 83 | } 84 | 85 | ul, ol { 86 | padding-left: 1.5rem; 87 | } 88 | 89 | dd { 90 | margin-left: 1.5rem; 91 | } 92 | 93 | blockquote { 94 | margin-left: 0; 95 | margin-right: 0; 96 | padding: .5rem 1rem; 97 | border-left: .25rem solid #ccc; 98 | color: #666; 99 | } 100 | 101 | blockquote p:last-child { 102 | margin-bottom: 0; 103 | } 104 | 105 | hr { 106 | border: none; 107 | margin: 1.5rem 0; 108 | border-bottom: 1px solid #ccc; 109 | position: relative; 110 | top: -1px; 111 | } 112 | 113 | .container img, .container iframe { 114 | max-width: 100%; 115 | } 116 | 117 | .container img { 118 | margin: 0 auto; 119 | display: block; 120 | } 121 | 122 | table { 123 | width: 100%; 124 | border: 1px solid #ccc; 125 | border-collapse: collapse; 126 | } 127 | 128 | td, th { 129 | padding: .25rem .5rem; 130 | border: 1px solid #ccc; 131 | } 132 | 133 | pre, code { 134 | font-family: inherit; 135 | background-color: #eee; 136 | } 137 | 138 | pre { 139 | padding: .5rem 1rem; 140 | font-size: 0.8rem; 141 | } 142 | 143 | code { 144 | padding: .1rem .25rem; 145 | } 146 | 147 | pre > code { 148 | padding: 0; 149 | } 150 | 151 | .container { 152 | max-width: 30rem; 153 | margin: 0 auto; 154 | padding: 0 1rem; 155 | } 156 | 157 | .middle-text { 158 | font-size: 0.9rem; 159 | } 160 | 161 | .small-text { 162 | font-size: 0.8rem; 163 | } 164 | 165 | .thanks { 166 | word-break: keep-all; 167 | } -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.7) 5 | base64 6 | nkf 7 | rexml 8 | activesupport (7.1.5.1) 9 | base64 10 | benchmark (>= 0.3) 11 | bigdecimal 12 | concurrent-ruby (~> 1.0, >= 1.0.2) 13 | connection_pool (>= 2.2.5) 14 | drb 15 | i18n (>= 1.6, < 2) 16 | logger (>= 1.4.2) 17 | minitest (>= 5.1) 18 | mutex_m 19 | securerandom (>= 0.3) 20 | tzinfo (~> 2.0) 21 | addressable (2.8.7) 22 | public_suffix (>= 2.0.2, < 7.0) 23 | algoliasearch (1.27.5) 24 | httpclient (~> 2.8, >= 2.8.3) 25 | json (>= 1.5.1) 26 | atomos (0.1.3) 27 | base64 (0.2.0) 28 | benchmark (0.4.0) 29 | bigdecimal (3.1.9) 30 | claide (1.1.0) 31 | cocoapods (1.16.2) 32 | addressable (~> 2.8) 33 | claide (>= 1.0.2, < 2.0) 34 | cocoapods-core (= 1.16.2) 35 | cocoapods-deintegrate (>= 1.0.3, < 2.0) 36 | cocoapods-downloader (>= 2.1, < 3.0) 37 | cocoapods-plugins (>= 1.0.0, < 2.0) 38 | cocoapods-search (>= 1.0.0, < 2.0) 39 | cocoapods-trunk (>= 1.6.0, < 2.0) 40 | cocoapods-try (>= 1.1.0, < 2.0) 41 | colored2 (~> 3.1) 42 | escape (~> 0.0.4) 43 | fourflusher (>= 2.3.0, < 3.0) 44 | gh_inspector (~> 1.0) 45 | molinillo (~> 0.8.0) 46 | nap (~> 1.0) 47 | ruby-macho (>= 2.3.0, < 3.0) 48 | xcodeproj (>= 1.27.0, < 2.0) 49 | cocoapods-core (1.16.2) 50 | activesupport (>= 5.0, < 8) 51 | addressable (~> 2.8) 52 | algoliasearch (~> 1.0) 53 | concurrent-ruby (~> 1.1) 54 | fuzzy_match (~> 2.0.4) 55 | nap (~> 1.0) 56 | netrc (~> 0.11) 57 | public_suffix (~> 4.0) 58 | typhoeus (~> 1.0) 59 | cocoapods-deintegrate (1.0.5) 60 | cocoapods-downloader (2.1) 61 | cocoapods-plugins (1.0.0) 62 | nap 63 | cocoapods-search (1.0.1) 64 | cocoapods-trunk (1.6.0) 65 | nap (>= 0.8, < 2.0) 66 | netrc (~> 0.11) 67 | cocoapods-try (1.2.0) 68 | colored2 (3.1.2) 69 | concurrent-ruby (1.3.5) 70 | connection_pool (2.5.0) 71 | drb (2.2.1) 72 | escape (0.0.4) 73 | ethon (0.16.0) 74 | ffi (>= 1.15.0) 75 | ffi (1.17.1) 76 | fourflusher (2.3.1) 77 | fuzzy_match (2.0.4) 78 | gh_inspector (1.1.3) 79 | httpclient (2.9.0) 80 | mutex_m 81 | i18n (1.14.7) 82 | concurrent-ruby (~> 1.0) 83 | json (2.10.2) 84 | logger (1.7.0) 85 | minitest (5.25.5) 86 | molinillo (0.8.0) 87 | mutex_m (0.3.0) 88 | nanaimo (0.4.0) 89 | nap (1.1.0) 90 | netrc (0.11.0) 91 | nkf (0.2.0) 92 | public_suffix (4.0.7) 93 | rexml (3.4.1) 94 | rouge (2.0.7) 95 | ruby-macho (2.5.1) 96 | securerandom (0.3.2) 97 | typhoeus (1.4.1) 98 | ethon (>= 0.9.0) 99 | tzinfo (2.0.6) 100 | concurrent-ruby (~> 1.0) 101 | xcodeproj (1.27.0) 102 | CFPropertyList (>= 2.3.3, < 4.0) 103 | atomos (~> 0.1.3) 104 | claide (>= 1.0.2, < 2.0) 105 | colored2 (~> 3.1) 106 | nanaimo (~> 0.4.0) 107 | rexml (>= 3.3.6, < 4.0) 108 | xcpretty (0.3.0) 109 | rouge (~> 2.0.7) 110 | 111 | PLATFORMS 112 | ruby 113 | 114 | DEPENDENCIES 115 | cocoapods (= 1.16.2) 116 | xcpretty (= 0.3.0) 117 | 118 | BUNDLED WITH 119 | 1.17.1 120 | -------------------------------------------------------------------------------- /Hakumai/Controllers/MainWindowController/TableCellView/TimeTableCellView.xib: -------------------------------------------------------------------------------- 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 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /Hakumai/Managers/MessageContainer/Message.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Message.swift 3 | // Hakumai 4 | // 5 | // Created by Hiroyuki Onishi on 12/3/14. 6 | // Copyright (c) 2014 Hiroyuki Onishi. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct Message { 12 | // MARK: - Types 13 | enum ContentType { 14 | case system(SystemMessage) 15 | case chat(ChatMessage) 16 | case debug(DebugMessage) 17 | } 18 | 19 | // MARK: - Properties 20 | let messageNo: Int 21 | let content: ContentType 22 | let date: Date = Date() 23 | 24 | // MARK: - Object Lifecycle 25 | init(messageNo: Int, system: String) { 26 | self.messageNo = messageNo 27 | self.content = .system(SystemMessage(message: system)) 28 | } 29 | 30 | init(messageNo: Int, chat: Chat, isFirst: Bool = false) { 31 | self.messageNo = messageNo 32 | self.content = .chat(chat.toChatMessage(isFirst: isFirst)) 33 | } 34 | 35 | init(messageNo: Int, debug: String) { 36 | self.messageNo = messageNo 37 | self.content = .debug(DebugMessage(message: debug)) 38 | } 39 | } 40 | 41 | extension Message { 42 | var isGift: Bool { giftImageUrl != nil } 43 | 44 | var giftImageUrl: URL? { 45 | switch content { 46 | case .chat(let chat): 47 | switch chat.chatType { 48 | case .gift(imageUrl: let imageUrl): 49 | return imageUrl 50 | case .comment, .nicoad, .other: 51 | return nil 52 | } 53 | default: 54 | return nil 55 | } 56 | } 57 | 58 | var isAd: Bool { 59 | switch content { 60 | case .chat(let chat): 61 | switch chat.chatType { 62 | case .nicoad: 63 | return true 64 | case .comment, .gift, .other: 65 | return false 66 | } 67 | default: 68 | return false 69 | } 70 | } 71 | } 72 | 73 | // MARK: - Individual Message 74 | struct SystemMessage { 75 | let message: String 76 | } 77 | 78 | struct ChatMessage { 79 | let roomPosition: RoomPosition 80 | let no: Int 81 | let date: Date 82 | let userId: String 83 | let comment: String 84 | let premium: Premium 85 | let isFirst: Bool 86 | let chatType: ChatType 87 | } 88 | 89 | struct DebugMessage { 90 | let message: String 91 | } 92 | 93 | // MARK: - ChatMessage Extension 94 | extension ChatMessage { 95 | var isRawUserId: Bool { userId.isRawUserId } 96 | var isUser: Bool { premium.isUser } 97 | var isSystem: Bool { premium.isSystem } 98 | var hasUserIcon: Bool { isUser && isRawUserId } 99 | var isCasterComment: Bool { premium == .caster } 100 | } 101 | 102 | extension String { 103 | func htmlTagRemoved(premium: Premium) -> String { 104 | guard premium == .caster, hasRegexp(pattern: "https?://") else { return self } 105 | return stringByRemovingRegexp(pattern: "<[^>]*>") 106 | } 107 | } 108 | 109 | // MARK: - Model Mapper 110 | extension Chat { 111 | func toChatMessage(isFirst: Bool) -> ChatMessage { 112 | return ChatMessage( 113 | roomPosition: roomPosition, 114 | no: no, 115 | date: date, 116 | userId: userId, 117 | comment: comment 118 | .htmlTagRemoved(premium: premium), 119 | premium: premium, 120 | isFirst: isFirst, 121 | chatType: chatType 122 | ) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /Hakumai/Controllers/MainWindowController/TableCellView/PremiumTableCellView.xib: -------------------------------------------------------------------------------- 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 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /Hakumai/Managers/NicoManager/NicoManagerProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NicoManagerProtocol.swift 3 | // Hakumai 4 | // 5 | // Created by Hiroyuki Onishi on 2021/05/16. 6 | // Copyright © 2021 Hiroyuki Onishi. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: - Protocol 12 | protocol NicoManagerType: AnyObject { 13 | // Properties 14 | var delegate: NicoManagerDelegate? { get set } 15 | var live: Live? { get } 16 | 17 | // Main Methods 18 | func connect(liveProgramId: String) 19 | func disconnect() 20 | func reconnect(reason: NicoReconnectReason) 21 | func comment(_ comment: String, anonymously: Bool, completion: @escaping (_ comment: String?) -> Void) 22 | func logout() 23 | 24 | // Methods for User Accounts 25 | func cachedUserName(for userId: String) -> String? 26 | func resolveUsername(for userId: String, completion: @escaping (String?) -> Void) 27 | func userPageUrl(for userId: String) -> URL? 28 | func userIconUrl(for userId: String) -> URL? 29 | 30 | // Misc Methods 31 | func livePageUrl(for liveProgramId: String) -> URL? 32 | func communityPageUrl(for communityId: String) -> URL? 33 | func adPageUrl(for liveProgramId: String) -> URL? 34 | func giftPageUrl(for communityId: String) -> URL? 35 | 36 | // Debug Methods 37 | func injectExpiredAccessToken() 38 | } 39 | 40 | // Note these functions are called in background thread, not main thread. 41 | // So use explicit main thread for updating UI components from these callbacks. 42 | protocol NicoManagerDelegate: AnyObject { 43 | // Token check results before proceeding to main connection sequence. 44 | func nicoManagerNeedsToken(_ nicoManager: NicoManagerType) 45 | func nicoManagerDidConfirmTokenExistence(_ nicoManager: NicoManagerType) 46 | 47 | // Main connection sequence. 48 | func nicoManagerWillPrepareLive(_ nicoManager: NicoManagerType) 49 | func nicoManagerDidPrepareLive(_ nicoManager: NicoManagerType, user: User, live: Live, connectContext: NicoConnectContext) 50 | func nicoManagerDidFailToPrepareLive(_ nicoManager: NicoManagerType, error: NicoError) 51 | func nicoManagerDidConnectToLive(_ nicoManager: NicoManagerType, roomPosition: RoomPosition, connectContext: NicoConnectContext) 52 | 53 | // Events after connection establishment. 54 | func nicoManagerDidReceiveChat(_ nicoManager: NicoManagerType, chat: Chat) 55 | func nicoManagerWillReconnectToLive(_ nicoManager: NicoManagerType, reason: NicoReconnectReason) 56 | func nicoManagerDidReceiveStatistics(_ nicoManager: NicoManagerType, stat: LiveStatistics) 57 | 58 | // History. 59 | func nicoManagerReceivingChatHistory(_ nicoManager: NicoManagerType, requestCount: Int, totalChatCount: Int) 60 | func nicoManagerDidReceiveChatHistory(_ nicoManager: NicoManagerType, chats: [Chat]) 61 | 62 | // Disconnect. 63 | func nicoManagerDidDisconnect(_ nicoManager: NicoManagerType, disconnectContext: NicoDisconnectContext) 64 | 65 | // Debug. 66 | func nicoManager(_ nicoManager: NicoManagerType, hasDebugMessgae message: String) 67 | } 68 | 69 | enum NicoError: Error { 70 | case `internal` 71 | case noLiveInfo 72 | case noMessageServerInfo 73 | case openMessageServerFailed 74 | case notStarted 75 | } 76 | 77 | enum NicoConnectContext { 78 | case normal 79 | case reconnect(NicoReconnectReason) 80 | 81 | var isReconnect: Bool { 82 | switch self { 83 | case .normal: 84 | return false 85 | case .reconnect: 86 | return true 87 | } 88 | } 89 | } 90 | 91 | enum NicoDisconnectContext { 92 | case normal 93 | case reconnect(NicoReconnectReason) 94 | } 95 | 96 | enum NicoReconnectReason { case normal, noPong, noTexts } 97 | -------------------------------------------------------------------------------- /Hakumai/Controllers/MainWindowController/TableCellView/UserIdTableCellView.xib: -------------------------------------------------------------------------------- 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 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /Hakumai/Controllers/AuthWindowController/AuthViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthViewController.swift 3 | // Hakumai 4 | // 5 | // Created by Hiroyuki Onishi on 2021/05/20. 6 | // Copyright © 2021 Hiroyuki Onishi. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import WebKit 11 | 12 | private let hakumaiAppUrlScheme = "hakumai" 13 | private let accessTokenParameterName = "response" 14 | 15 | protocol AuthViewControllerDelegate: AnyObject { 16 | func authViewControllerDidLogin(_ authViewController: AuthViewController) 17 | } 18 | 19 | final class AuthViewController: NSViewController { 20 | // MARK: - Properties 21 | @IBOutlet private weak var webView: WKWebView! 22 | 23 | private var authManager: AuthManagerProtocol! 24 | private weak var delegate: AuthViewControllerDelegate? 25 | } 26 | 27 | extension AuthViewController { 28 | override func viewDidLoad() { 29 | super.viewDidLoad() 30 | configureView() 31 | authManager = AuthManager.shared 32 | } 33 | } 34 | 35 | extension AuthViewController { 36 | func setDelegate(_ delegate: AuthViewControllerDelegate?) { 37 | self.delegate = delegate 38 | } 39 | 40 | func startAuthorization() { 41 | let request = URLRequest(url: authManager.authWebUrl) 42 | webView.load(request) 43 | } 44 | 45 | func clearAllCookies() { 46 | let dataStore = WKWebsiteDataStore.default() 47 | dataStore.fetchDataRecords(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes()) { records in 48 | dataStore.removeData( 49 | ofTypes: WKWebsiteDataStore.allWebsiteDataTypes(), 50 | for: records, 51 | completionHandler: {} 52 | ) 53 | } 54 | // https://stackoverflow.com/a/54573361/13220031 55 | DispatchQueue.main.async { 56 | self.webView.configuration.processPool = WKProcessPool() 57 | } 58 | } 59 | } 60 | 61 | extension AuthViewController: WKNavigationDelegate { 62 | func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { 63 | guard let url = navigationAction.request.url else { return } 64 | // log.debug(url) 65 | if url.scheme == hakumaiAppUrlScheme { 66 | extractCallbackResponse(from: url) 67 | decisionHandler(.cancel) 68 | closeWindow() 69 | return 70 | } 71 | decisionHandler(.allow) 72 | } 73 | } 74 | 75 | private extension AuthViewController { 76 | func configureView() { 77 | webView.navigationDelegate = self 78 | } 79 | 80 | func extractCallbackResponse(from url: URL) { 81 | guard let response = url.queryValue(for: accessTokenParameterName) else { return } 82 | // log.debug(response) 83 | authManager.extractCallbackResponseAndSaveToken(response: response) { [weak self] in 84 | guard let me = self else { return } 85 | log.debug($0) 86 | switch $0 { 87 | case .success: 88 | me.delegate?.authViewControllerDidLogin(me) 89 | case .failure(let error): 90 | log.error(error) 91 | } 92 | } 93 | } 94 | 95 | func closeWindow() { 96 | webView.loadHTMLString("", baseURL: nil) 97 | view.window?.close() 98 | } 99 | } 100 | 101 | private extension URL { 102 | // https://qiita.com/shtnkgm/items/0f69d8000f10bdf7cbe2 103 | func queryValue(for key: String) -> String? { 104 | let queryItems = URLComponents(string: absoluteString)?.queryItems 105 | return queryItems?.filter { $0.name == key }.compactMap { $0.value }.first 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /Hakumai/Controllers/MainWindowController/TableCellView/RoomPositionTableCellView.xib: -------------------------------------------------------------------------------- 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 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /Hakumai/Controllers/MainWindowController/TableCellView/UserIdTableCellView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserIdTableCellView.swift 3 | // Hakumai 4 | // 5 | // Created by Hiroyuki Onishi on 12/2/14. 6 | // Copyright (c) 2014 Hiroyuki Onishi. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import AppKit 11 | 12 | private let systemUserLabel = "----------" 13 | 14 | final class UserIdTableCellView: NSTableCellView { 15 | @IBOutlet weak var userIdTextField: NSTextField! 16 | @IBOutlet weak var userIdImageView: NSImageView! 17 | 18 | var fontSize: CGFloat? { didSet { set(fontSize: fontSize) } } 19 | 20 | // XXX: remove this non presentation layer instance.. 21 | private var nicoManager: NicoManagerType? 22 | private var currentUserId: String? 23 | } 24 | 25 | extension UserIdTableCellView { 26 | func configure(info: (nicoManager: NicoManagerType, handleName: String?, userId: String?, premium: Premium?, comment: String?)?) { 27 | nicoManager = info?.nicoManager 28 | currentUserId = info?.userId 29 | guard let userId = info?.userId, let premium = info?.premium else { 30 | userIdImageView.image = nil 31 | userIdTextField.stringValue = "" 32 | return 33 | } 34 | userIdImageView.image = image(forHandleName: info?.handleName, userId: userId, premium: premium) 35 | setUserIdLabel(userId: userId, premium: premium, handleName: info?.handleName) 36 | } 37 | } 38 | 39 | private extension UserIdTableCellView { 40 | func image(forHandleName handleName: String?, userId: String, premium: Premium) -> NSImage { 41 | if premium.isSystem { 42 | return Asset.premiumMisc.image 43 | } else if handleName != nil { 44 | return userId.isRawUserId ? 45 | Asset.handleNameOverRawId.image : Asset.handleNameOver184Id.image 46 | } 47 | return userId.isRawUserId ? 48 | Asset.userIdRawId.image : Asset.userId184Id.image 49 | } 50 | 51 | func setUserIdLabel(userId: String, premium: Premium, handleName: String?) { 52 | // set default name 53 | userIdTextField.stringValue = premium.isSystem ? 54 | systemUserLabel : 55 | concatUserName(userId: userId, userName: nil, handleName: handleName) 56 | 57 | // if needed, then resolve userid 58 | guard handleName == nil, premium.isUser, userId.isRawUserId else { return } 59 | 60 | if let userName = nicoManager?.cachedUserName(for: userId) { 61 | userIdTextField.stringValue = concatUserName(userId: userId, userName: userName, handleName: handleName) 62 | return 63 | } 64 | 65 | nicoManager?.resolveUsername(for: userId) { [weak self] in 66 | guard let me = self else { return } 67 | guard me.currentUserId == userId else { 68 | // Seems the view is reused before the previous async username 69 | // resolving operation from this view is finished. So skip... 70 | log.debug("Skip updating cell user name.") 71 | return 72 | } 73 | guard let userName = $0 else { return } 74 | DispatchQueue.main.async { 75 | me.userIdTextField.stringValue = 76 | me.concatUserName(userId: userId, userName: userName, handleName: handleName) 77 | } 78 | } 79 | } 80 | 81 | func concatUserName(userId: String, userName: String?, handleName: String?) -> String { 82 | let concatenated: String 83 | if let handleName = handleName { 84 | concatenated = handleName + " (" + userId + ")" 85 | } else if let userName = userName { 86 | concatenated = userName + " (" + userId + ")" 87 | } else { 88 | concatenated = userId 89 | } 90 | return concatenated 91 | } 92 | 93 | func set(fontSize: CGFloat?) { 94 | let size = fontSize ?? CGFloat(kDefaultFontSize) 95 | userIdTextField.font = NSFont.systemFont(ofSize: size) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /HakumaiTests/Extensions/StringExtensionTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CommonExtensionsTests.swift 3 | // Hakumai 4 | // 5 | // Created by Hiroyuki Onishi on 11/17/14. 6 | // Copyright (c) 2014 Hiroyuki Onishi. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import XCTest 11 | @testable import Hakumai 12 | 13 | final class StringExtensionTests: XCTestCase { 14 | override func setUp() {} 15 | override func tearDown() {} 16 | } 17 | 18 | // MARK: String 19 | extension StringExtensionTests { 20 | func testIsRawUserId() { 21 | XCTAssert("123".isRawUserId == true, "") 22 | XCTAssert("123a".isRawUserId == false, "") 23 | } 24 | 25 | func testExtractRegexp() { 26 | var pattern: String 27 | var extracted: String? 28 | 29 | pattern = "http:\\/\\/live\\.nicovideo\\.jp\\/watch\\/lv(\\d{5,}).*" 30 | extracted = "http://live.nicovideo.jp/watch/lv200433812?ref=zero_mynicorepo".extractRegexp(pattern: pattern) 31 | XCTAssert(extracted == "200433812", "") 32 | 33 | /* 34 | pattern = "(http:\\/\\/live\\.nicovideo\\.jp\\/watch\\/)?(lv)?(\\d+).*" 35 | extracted = "http://live.nicovideo.jp/watch/lv200433812?ref=zero_mynicorepo".extractRegexp(pattern: pattern, index: 0) 36 | XCTAssert(extracted == "200433812", "") 37 | */ 38 | } 39 | 40 | func testHasRegexp() { 41 | XCTAssert("abc".hasRegexp(pattern: "b") == true, "") 42 | XCTAssert("abc".hasRegexp(pattern: "1") == false, "") 43 | 44 | // half-width character with (han)daku-on case. http://stackoverflow.com/a/27192734 45 | XCTAssert("ハデw".hasRegexp(pattern: "w") == true, "") 46 | } 47 | 48 | func testStringByRemovingRegexp() { 49 | var removed: String 50 | 51 | removed = "abcd".stringByRemovingRegexp(pattern: "bc") 52 | XCTAssert(removed == "ad", "") 53 | 54 | removed = "abcdabcd".stringByRemovingRegexp(pattern: "bc") 55 | XCTAssert(removed == "adad", "") 56 | 57 | removed = "abc\n".stringByRemovingRegexp(pattern: "\n") 58 | XCTAssert(removed == "abc", "") 59 | } 60 | } 61 | 62 | extension StringExtensionTests { 63 | func testExtractLiveNumber() { 64 | var extracted: String? 65 | let expected = "lv200433812" 66 | 67 | extracted = "http://live.nicovideo.jp/watch/lv200433812?ref=zero_mynicorepo".extractLiveProgramId() 68 | XCTAssert(extracted == expected, "") 69 | 70 | extracted = "http://live.nicovideo.jp/watch/lv200433812".extractLiveProgramId() 71 | XCTAssert(extracted == expected, "") 72 | 73 | extracted = "lv200433812".extractLiveProgramId() 74 | XCTAssert(extracted == expected, "") 75 | } 76 | 77 | func testUrlStringInComment() { 78 | var comment = "" 79 | 80 | comment = "aaa" 81 | XCTAssert(comment.extractUrlString() == nil, "") 82 | 83 | comment = "aaa http://example.com aaa" 84 | XCTAssert(comment.extractUrlString() == "http://example.com", "") 85 | } 86 | 87 | func testIsValidHexString() { 88 | XCTAssert("".isValidHexString == false, "") 89 | XCTAssert("123".isValidHexString == false, "") 90 | XCTAssert("#000000".isValidHexString == true, "") 91 | XCTAssert("#123456".isValidHexString == true, "") 92 | XCTAssert("#abcdef".isValidHexString == true, "") 93 | XCTAssert("#ABCDEF".isValidHexString == true, "") 94 | XCTAssert("#789abc".isValidHexString == true, "") 95 | XCTAssert("#789ABC".isValidHexString == true, "") 96 | XCTAssert("#ffffff".isValidHexString == true, "") 97 | XCTAssert("#FFFFFF".isValidHexString == true, "") 98 | XCTAssert("#000".isValidHexString == false, "") 99 | XCTAssert("#fff".isValidHexString == false, "") 100 | XCTAssert("#FFF".isValidHexString == false, "") 101 | XCTAssert("#1234567".isValidHexString == false, "") 102 | XCTAssert("#fffffff".isValidHexString == false, "") 103 | XCTAssert("#FFFFFFF".isValidHexString == false, "") 104 | } 105 | } 106 | --------------------------------------------------------------------------------