├── 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 |
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 | [](https://swift.org/)
4 | [](http://www.opensource.org/licenses/mit-license.php)
5 | [](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 |
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 |
--------------------------------------------------------------------------------