├── documents ├── .gitignore ├── en-US │ └── images │ │ └── name.png ├── zh-Hans │ ├── images │ │ └── name.png │ └── README.md └── assets │ ├── download_from_appstore.png │ └── download_from_google_play.png ├── app ├── android │ ├── settings_aar.gradle │ ├── gradle.properties │ ├── app │ │ └── src │ │ │ ├── main │ │ │ ├── res │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── values │ │ │ │ │ ├── colors.xml │ │ │ │ │ └── styles.xml │ │ │ │ ├── drawable-hdpi │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── drawable-mdpi │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── drawable-xhdpi │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── drawable-xxhdpi │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── drawable-xxxhdpi │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ │ └── ic_launcher.xml │ │ │ │ └── drawable │ │ │ │ │ └── launch_background.xml │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── bangumin │ │ │ │ └── munin │ │ │ │ └── MainActivity.java │ │ │ ├── profile │ │ │ └── AndroidManifest.xml │ │ │ └── debug │ │ │ └── AndroidManifest.xml │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ ├── settings.gradle │ └── build.gradle ├── test │ ├── integration │ │ └── http │ │ │ ├── .gitignore │ │ │ ├── readme.md │ │ │ └── config.json.example │ ├── Widget_test.dart │ ├── shared │ │ └── utils │ │ │ ├── common_test.dart │ │ │ └── misc │ │ │ └── DeviceInfo_test.dart │ └── AppCast_test.dart ├── ios │ ├── Flutter │ │ ├── .last_build_id │ │ ├── Uat.xcconfig │ │ ├── Debug.xcconfig │ │ ├── Release.xcconfig │ │ └── AppFrameworkInfo.plist │ ├── Runner │ │ ├── Runner-Bridging-Header.h │ │ ├── Assets.xcassets │ │ │ ├── Contents.json │ │ │ ├── LaunchImage.imageset │ │ │ │ ├── launch_128x128.png │ │ │ │ ├── launch_256x256.png │ │ │ │ └── launch_384x384.png │ │ │ ├── AppIcon.appiconset │ │ │ │ ├── Icon-App-20x20@1x.png │ │ │ │ ├── Icon-App-20x20@2x.png │ │ │ │ ├── Icon-App-20x20@3x.png │ │ │ │ ├── Icon-App-29x29@1x.png │ │ │ │ ├── Icon-App-29x29@2x.png │ │ │ │ ├── Icon-App-29x29@3x.png │ │ │ │ ├── Icon-App-40x40@1x.png │ │ │ │ ├── Icon-App-40x40@2x.png │ │ │ │ ├── Icon-App-40x40@3x.png │ │ │ │ ├── Icon-App-60x60@2x.png │ │ │ │ ├── Icon-App-60x60@3x.png │ │ │ │ ├── Icon-App-76x76@1x.png │ │ │ │ ├── Icon-App-76x76@2x.png │ │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ │ └── Icon-App-83.5x83.5@2x.png │ │ │ └── SplashScreenBackgroundColor.colorset │ │ │ │ └── Contents.json │ │ ├── AppDelegate.swift │ │ └── GoogleService-Info.plist │ ├── build │ │ └── Runner.build │ │ │ ├── Debug-iphoneos │ │ │ └── Runner.build │ │ │ │ └── dgph │ │ │ ├── Profile-iphoneos │ │ │ └── Runner.build │ │ │ │ └── dgph │ │ │ ├── Debug-iphonesimulator │ │ │ └── Runner.build │ │ │ │ └── dgph │ │ │ └── Release-production-iphoneos │ │ │ └── Runner.build │ │ │ └── dgph │ ├── Runner.xcodeproj │ │ └── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── Runner.xcworkspace │ │ ├── xcshareddata │ │ ├── WorkspaceSettings.xcsettings │ │ └── IDEWorkspaceChecks.plist │ │ └── contents.xcworkspacedata ├── lib │ ├── widgets │ │ ├── timeline │ │ │ ├── message │ │ │ │ └── Common.dart │ │ │ └── item │ │ │ │ ├── UnknownTimelineActivityWidget.dart │ │ │ │ ├── PublicMessageNoReplyWidget.dart │ │ │ │ └── common │ │ │ │ └── Actions.dart │ │ ├── shared │ │ │ ├── text │ │ │ │ ├── editor │ │ │ │ │ ├── common.dart │ │ │ │ │ ├── showBangumiStickersBottomSheet.dart │ │ │ │ │ └── sticker │ │ │ │ │ │ ├── utils.dart │ │ │ │ │ │ └── BangumiStickers.dart │ │ │ │ └── MuninTextSpans.dart │ │ │ ├── common │ │ │ │ ├── Divider.dart │ │ │ │ ├── SingleChildExpandedRow.dart │ │ │ │ ├── HorizontalScrollableWidget.dart │ │ │ │ ├── ScaffoldWithRegularAppBar.dart │ │ │ │ └── SnackBar.dart │ │ │ ├── utils │ │ │ │ ├── ExpandedEmpty.dart │ │ │ │ └── Scroll.dart │ │ │ ├── bottomsheet │ │ │ │ └── showMinHeightModalBottomSheet.dart │ │ │ ├── icons │ │ │ │ └── MuninSmallTriangle.dart │ │ │ ├── button │ │ │ │ ├── customization.dart │ │ │ │ ├── FilledFlatButton.dart │ │ │ │ ├── RoundedInkWell.dart │ │ │ │ └── MuninOutlineButton.dart │ │ │ ├── background │ │ │ │ ├── GreyRoundedBorderContainer.dart │ │ │ │ └── RoundedConcreteBackgroundWithChild.dart │ │ │ ├── cover │ │ │ │ └── ClickableCachedRoundedCover.dart │ │ │ └── chips │ │ │ │ └── StrokeChip.dart │ │ ├── discussion │ │ │ └── thread │ │ │ │ └── shared │ │ │ │ ├── Constants.dart │ │ │ │ └── CopyPostContent.dart │ │ ├── subject │ │ │ ├── info │ │ │ │ └── SubjectInfoBottomSheet.dart │ │ │ ├── common │ │ │ │ └── Common.dart │ │ │ ├── mainpage │ │ │ │ ├── CharactersPreview.dart │ │ │ │ └── RelatedSubjectsPreview.dart │ │ │ └── management │ │ │ │ └── StarRatingFormField.dart │ │ ├── initial │ │ │ └── splash.dart │ │ ├── search │ │ │ └── UserSearchResultWidget.dart │ │ ├── setting │ │ │ ├── Common.dart │ │ │ ├── theme │ │ │ │ └── Common.dart │ │ │ ├── mute │ │ │ │ └── HowToAdd.dart │ │ │ └── general │ │ │ │ └── BrowserSettingWidget.dart │ │ ├── home │ │ │ └── HomePageAppBarTitle.dart │ │ └── user │ │ │ └── UserHome.dart │ ├── models │ │ └── bangumi │ │ │ ├── discussion │ │ │ ├── enums │ │ │ │ ├── base.dart │ │ │ │ └── DiscussionType.dart │ │ │ ├── DiscussionItem.dart │ │ │ ├── thread │ │ │ │ └── common │ │ │ │ │ ├── BangumiThread.g.dart │ │ │ │ │ ├── GetThreadRequest.dart │ │ │ │ │ ├── OriginalPost.dart │ │ │ │ │ └── Post.g.dart │ │ │ ├── GroupDiscussionItem.dart │ │ │ ├── GeneralDiscussionItem.dart │ │ │ └── DiscussionItem.g.dart │ │ │ ├── common │ │ │ ├── ChineseNameOwner.dart │ │ │ └── ItemMetaInfo.dart │ │ │ ├── timeline │ │ │ ├── common │ │ │ │ ├── HyperItem.dart │ │ │ │ ├── FeedLoadType.dart │ │ │ │ ├── FeedMetaInfo.dart │ │ │ │ ├── TimelineFeed.dart │ │ │ │ ├── HyperImage.dart │ │ │ │ ├── Mono.dart │ │ │ │ ├── TimelineFeed.g.dart │ │ │ │ └── HyperBangumiItem.dart │ │ │ ├── WikiCreationSingle.dart │ │ │ ├── ProgressUpdateEpisodeUntil.dart │ │ │ ├── GroupJoinSingle.dart │ │ │ ├── BlogCreationSingle.dart │ │ │ ├── PublicMessageNoReply.dart │ │ │ ├── IndexFavoriteSingle.dart │ │ │ ├── UnknownTimelineActivity.dart │ │ │ ├── ProgressUpdateEpisodeSingle.dart │ │ │ ├── FriendshipCreationSingle.dart │ │ │ ├── PublicMessageNormal.dart │ │ │ ├── MonoFavoriteSingle.dart │ │ │ ├── CollectionUpdateSingle.dart │ │ │ └── message │ │ │ │ └── PublicMessageReply.dart │ │ │ ├── mono │ │ │ ├── MonoBase.dart │ │ │ ├── Actor.dart │ │ │ └── MonoBase.g.dart │ │ │ ├── search │ │ │ ├── result │ │ │ │ ├── SearchResultItem.dart │ │ │ │ ├── BangumiSearchResponse.dart │ │ │ │ ├── SearchResultItem.g.dart │ │ │ │ ├── BangumiSearchResponse.g.dart │ │ │ │ └── MonoSearchResult.dart │ │ │ └── SearchRequest.dart │ │ │ ├── progress │ │ │ ├── common │ │ │ │ ├── InProgressCollection.dart │ │ │ │ ├── InProgressCollection.g.dart │ │ │ │ ├── BaseEpisode.dart │ │ │ │ └── BaseEpisode.g.dart │ │ │ └── html │ │ │ │ └── SubjectEpisodes.dart │ │ │ ├── BangumiOauthCredentials.dart │ │ │ ├── user │ │ │ ├── social │ │ │ │ ├── NetworkServiceTag.dart │ │ │ │ ├── NetworkServiceTag.g.dart │ │ │ │ ├── NetworkServiceTagLink.dart │ │ │ │ └── NetworkServiceTagPlainText.dart │ │ │ ├── notification │ │ │ │ ├── BaseNotificationItem.dart │ │ │ │ ├── BaseNotificationItem.g.dart │ │ │ │ └── GeneralNotificationItem.dart │ │ │ └── timeline │ │ │ │ └── TimelinePreview.dart │ │ │ ├── subject │ │ │ ├── review │ │ │ │ ├── ReviewMetaInfo.dart │ │ │ │ └── SubjectReview.dart │ │ │ ├── common │ │ │ │ ├── SujectBase.dart │ │ │ │ ├── SubjectStatus.dart │ │ │ │ ├── SujectBase.g.dart │ │ │ │ ├── ParentSubject.dart │ │ │ │ └── SubjectBaseWithCover.dart │ │ │ ├── RelatedSubject.dart │ │ │ └── info │ │ │ │ └── InfoBoxItem.dart │ │ │ ├── setting │ │ │ ├── theme │ │ │ │ └── ThemeSwitchMode.dart │ │ │ └── general │ │ │ │ └── browser │ │ │ │ └── BrowserSetting.dart │ │ │ ├── BangumiCookieCredentials.dart │ │ │ └── BangumiUserIdentity.dart │ ├── config │ │ ├── .gitignore │ │ ├── production_with_update_detector_example.dart │ │ ├── environment_example.dart │ │ └── upgrader │ │ │ ├── readme.md │ │ │ └── Utils.dart │ ├── providers │ │ ├── bangumi │ │ │ ├── readme.md │ │ │ ├── search │ │ │ │ └── parser │ │ │ │ │ └── isolate.dart │ │ │ ├── util │ │ │ │ ├── DioInterceptors.dart │ │ │ │ └── parser │ │ │ │ │ ├── ParseBangumiUser.dart │ │ │ │ │ └── Spoiler.dart │ │ │ └── progress │ │ │ │ └── parser │ │ │ │ └── isolate.dart │ │ ├── README.md │ │ └── storage │ │ │ └── SecureStorageService.dart │ ├── redux │ │ ├── setting │ │ │ └── Common.dart │ │ ├── shared │ │ │ ├── utils.dart │ │ │ └── RequestStatus.dart │ │ ├── oauth │ │ │ ├── OauthReducer.dart │ │ │ ├── OauthState.dart │ │ │ └── OauthActions.dart │ │ ├── app │ │ │ └── AppActions.dart │ │ ├── readme.md │ │ ├── search │ │ │ ├── SearchState.dart │ │ │ └── SearchReducer.dart │ │ └── progress │ │ │ └── common.dart │ ├── styles │ │ └── theme │ │ │ ├── CommonThemeData.dart │ │ │ ├── NightDeepGreyBlue.dart │ │ │ ├── BrightIonBrownBlue.dart │ │ │ ├── BrightNatoriPinkBrown.dart │ │ │ ├── BrightBlueBangumiPink.dart │ │ │ └── NightPureDarkBlue.dart │ └── shared │ │ └── utils │ │ ├── a11y │ │ └── common.dart │ │ ├── analytics │ │ └── Constants.dart │ │ ├── http │ │ └── common.dart │ │ ├── ui │ │ └── completers.dart │ │ └── misc │ │ ├── async.dart │ │ └── constants.dart ├── assets │ ├── stickers │ │ ├── bangumi │ │ │ ├── 01.png │ │ │ ├── 02.png │ │ │ ├── 03.png │ │ │ ├── 04.png │ │ │ ├── 05.png │ │ │ ├── 06.png │ │ │ ├── 07.png │ │ │ ├── 08.png │ │ │ ├── 09.png │ │ │ ├── 10.png │ │ │ ├── 100.gif │ │ │ ├── 101.gif │ │ │ ├── 102.gif │ │ │ ├── 103.gif │ │ │ ├── 104.gif │ │ │ ├── 105.gif │ │ │ ├── 106.gif │ │ │ ├── 107.gif │ │ │ ├── 108.gif │ │ │ ├── 109.gif │ │ │ ├── 11.gif │ │ │ ├── 110.gif │ │ │ ├── 111.gif │ │ │ ├── 112.gif │ │ │ ├── 113.gif │ │ │ ├── 114.gif │ │ │ ├── 115.gif │ │ │ ├── 116.gif │ │ │ ├── 117.gif │ │ │ ├── 118.gif │ │ │ ├── 119.gif │ │ │ ├── 12.png │ │ │ ├── 120.gif │ │ │ ├── 121.gif │ │ │ ├── 122.gif │ │ │ ├── 123.gif │ │ │ ├── 13.png │ │ │ ├── 14.png │ │ │ ├── 15.png │ │ │ ├── 16.png │ │ │ ├── 17.png │ │ │ ├── 18.png │ │ │ ├── 19.png │ │ │ ├── 20.png │ │ │ ├── 21.png │ │ │ ├── 22.png │ │ │ ├── 23.gif │ │ │ ├── 24.gif │ │ │ ├── 25.gif │ │ │ ├── 26.gif │ │ │ ├── 27.gif │ │ │ ├── 28.gif │ │ │ ├── 29.gif │ │ │ ├── 30.gif │ │ │ ├── 31.gif │ │ │ ├── 32.gif │ │ │ ├── 33.gif │ │ │ ├── 34.gif │ │ │ ├── 35.gif │ │ │ ├── 36.gif │ │ │ ├── 37.gif │ │ │ ├── 38.gif │ │ │ ├── 39.gif │ │ │ ├── 40.gif │ │ │ ├── 41.gif │ │ │ ├── 42.gif │ │ │ ├── 43.gif │ │ │ ├── 44.gif │ │ │ ├── 45.gif │ │ │ ├── 46.gif │ │ │ ├── 47.gif │ │ │ ├── 48.gif │ │ │ ├── 49.gif │ │ │ ├── 50.gif │ │ │ ├── 51.gif │ │ │ ├── 52.gif │ │ │ ├── 53.gif │ │ │ ├── 54.gif │ │ │ ├── 55.gif │ │ │ ├── 56.gif │ │ │ ├── 57.gif │ │ │ ├── 58.gif │ │ │ ├── 59.gif │ │ │ ├── 60.gif │ │ │ ├── 61.gif │ │ │ ├── 62.gif │ │ │ ├── 63.gif │ │ │ ├── 64.gif │ │ │ ├── 65.gif │ │ │ ├── 66.gif │ │ │ ├── 67.gif │ │ │ ├── 68.gif │ │ │ ├── 69.gif │ │ │ ├── 70.gif │ │ │ ├── 71.gif │ │ │ ├── 72.gif │ │ │ ├── 73.gif │ │ │ ├── 74.gif │ │ │ ├── 75.gif │ │ │ ├── 76.gif │ │ │ ├── 77.gif │ │ │ ├── 78.gif │ │ │ ├── 79.gif │ │ │ ├── 80.gif │ │ │ ├── 81.gif │ │ │ ├── 82.gif │ │ │ ├── 83.gif │ │ │ ├── 84.gif │ │ │ ├── 85.gif │ │ │ ├── 86.gif │ │ │ ├── 87.gif │ │ │ ├── 88.gif │ │ │ ├── 89.gif │ │ │ ├── 90.gif │ │ │ ├── 91.gif │ │ │ ├── 92.gif │ │ │ ├── 93.gif │ │ │ ├── 94.gif │ │ │ ├── 95.gif │ │ │ ├── 96.gif │ │ │ ├── 97.gif │ │ │ ├── 98.gif │ │ │ └── 99.gif │ │ └── readme.md │ └── logo │ │ ├── munin_logo_full_2048x2048.png │ │ ├── munin_logo_full_512x512.png │ │ └── munin_logo_padding_rounded_2048x2048.png ├── fonts │ └── icons │ │ └── munin │ │ └── MuninIcons.ttf ├── document │ └── release │ │ ├── 0.3.1 │ │ └── images │ │ │ ├── card.jpg │ │ │ ├── mute.jpg │ │ │ ├── theme.jpg │ │ │ ├── progress.jpg │ │ │ ├── timeline.jpg │ │ │ ├── discussion.jpg │ │ │ ├── launch_switch.jpg │ │ │ └── general_setting.jpg │ │ └── 0.4.2 │ │ └── images │ │ ├── sort.jpg │ │ └── notification.jpg └── .metadata ├── .gitignore ├── LICENSE └── README.md /documents/.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | -------------------------------------------------------------------------------- /app/android/settings_aar.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | -------------------------------------------------------------------------------- /app/test/integration/http/.gitignore: -------------------------------------------------------------------------------- 1 | config.json 2 | -------------------------------------------------------------------------------- /app/ios/Flutter/.last_build_id: -------------------------------------------------------------------------------- 1 | 9cf3cfc1a955eae8e1fc249bd5d89508 -------------------------------------------------------------------------------- /app/ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" -------------------------------------------------------------------------------- /app/lib/widgets/timeline/message/Common.dart: -------------------------------------------------------------------------------- 1 | const onReplySuccessText = '回复成功'; 2 | -------------------------------------------------------------------------------- /app/lib/widgets/shared/text/editor/common.dart: -------------------------------------------------------------------------------- 1 | const editorMinLines = 10; 2 | const editorMaxLines = 100; 3 | -------------------------------------------------------------------------------- /documents/en-US/images/name.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/documents/en-US/images/name.png -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/01.png -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/02.png -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/03.png -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/04.png -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/05.png -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/06.png -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/07.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/07.png -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/08.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/08.png -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/09.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/09.png -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/10.png -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/100.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/100.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/101.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/101.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/102.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/102.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/103.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/103.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/104.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/104.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/105.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/105.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/106.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/106.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/107.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/107.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/108.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/108.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/109.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/109.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/11.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/11.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/110.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/110.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/111.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/111.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/112.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/112.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/113.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/113.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/114.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/114.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/115.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/115.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/116.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/116.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/117.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/117.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/118.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/118.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/119.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/119.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/12.png -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/120.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/120.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/121.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/121.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/122.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/122.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/123.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/123.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/13.png -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/14.png -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/15.png -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/16.png -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/17.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/17.png -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/18.png -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/19.png -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/20.png -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/21.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/21.png -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/22.png -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/23.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/23.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/24.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/24.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/25.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/25.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/26.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/26.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/27.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/27.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/28.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/28.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/29.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/29.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/30.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/30.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/31.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/31.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/32.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/32.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/33.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/33.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/34.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/34.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/35.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/35.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/36.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/36.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/37.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/37.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/38.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/38.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/39.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/39.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/40.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/40.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/41.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/41.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/42.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/42.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/43.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/43.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/44.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/44.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/45.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/45.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/46.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/46.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/47.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/47.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/48.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/48.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/49.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/49.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/50.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/50.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/51.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/51.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/52.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/52.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/53.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/53.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/54.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/54.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/55.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/55.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/56.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/56.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/57.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/57.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/58.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/58.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/59.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/59.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/60.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/60.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/61.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/61.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/62.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/62.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/63.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/63.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/64.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/64.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/65.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/65.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/66.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/66.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/67.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/67.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/68.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/68.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/69.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/69.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/70.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/70.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/71.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/71.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/72.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/72.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/73.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/73.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/74.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/74.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/75.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/75.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/76.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/76.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/77.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/77.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/78.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/78.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/79.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/79.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/80.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/80.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/81.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/81.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/82.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/82.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/83.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/83.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/84.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/84.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/85.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/85.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/86.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/86.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/87.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/87.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/88.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/88.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/89.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/89.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/90.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/90.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/91.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/91.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/92.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/92.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/93.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/93.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/94.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/94.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/95.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/95.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/96.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/96.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/97.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/97.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/98.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/98.gif -------------------------------------------------------------------------------- /app/assets/stickers/bangumi/99.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/stickers/bangumi/99.gif -------------------------------------------------------------------------------- /app/ios/Runner/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "version": 1, 4 | "author": "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /app/lib/models/bangumi/discussion/enums/base.dart: -------------------------------------------------------------------------------- 1 | abstract class DiscussionFilter { 2 | String get chineseName; 3 | } 4 | -------------------------------------------------------------------------------- /documents/zh-Hans/images/name.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/documents/zh-Hans/images/name.png -------------------------------------------------------------------------------- /app/fonts/icons/munin/MuninIcons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/fonts/icons/munin/MuninIcons.ttf -------------------------------------------------------------------------------- /app/lib/widgets/discussion/thread/shared/Constants.dart: -------------------------------------------------------------------------------- 1 | const userWithDefaultAvatarCannotPostReplyLabel = '未设置过头像的用户无法正常发表回复'; 2 | -------------------------------------------------------------------------------- /app/document/release/0.3.1/images/card.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/document/release/0.3.1/images/card.jpg -------------------------------------------------------------------------------- /app/document/release/0.3.1/images/mute.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/document/release/0.3.1/images/mute.jpg -------------------------------------------------------------------------------- /app/document/release/0.4.2/images/sort.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/document/release/0.4.2/images/sort.jpg -------------------------------------------------------------------------------- /app/assets/logo/munin_logo_full_2048x2048.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/logo/munin_logo_full_2048x2048.png -------------------------------------------------------------------------------- /app/assets/logo/munin_logo_full_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/logo/munin_logo_full_512x512.png -------------------------------------------------------------------------------- /app/document/release/0.3.1/images/theme.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/document/release/0.3.1/images/theme.jpg -------------------------------------------------------------------------------- /documents/assets/download_from_appstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/documents/assets/download_from_appstore.png -------------------------------------------------------------------------------- /app/document/release/0.3.1/images/progress.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/document/release/0.3.1/images/progress.jpg -------------------------------------------------------------------------------- /app/document/release/0.3.1/images/timeline.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/document/release/0.3.1/images/timeline.jpg -------------------------------------------------------------------------------- /app/ios/build/Runner.build/Debug-iphoneos/Runner.build/dgph: -------------------------------------------------------------------------------- 1 | DGPH1.04 Oct 21 202221:59:43/UsersedwardcodeBangumiNappios -------------------------------------------------------------------------------- /app/ios/build/Runner.build/Profile-iphoneos/Runner.build/dgph: -------------------------------------------------------------------------------- 1 | DGPH1.04 Dec 14 202110:05:20/UsersedwardcodeBangumiNappios -------------------------------------------------------------------------------- /documents/assets/download_from_google_play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/documents/assets/download_from_google_play.png -------------------------------------------------------------------------------- /app/android/gradle.properties: -------------------------------------------------------------------------------- 1 | android.enableJetifier=true 2 | android.useAndroidX=true 3 | org.gradle.jvmargs=-Xmx1536M 4 | android.enableR8=true 5 | -------------------------------------------------------------------------------- /app/document/release/0.3.1/images/discussion.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/document/release/0.3.1/images/discussion.jpg -------------------------------------------------------------------------------- /app/document/release/0.4.2/images/notification.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/document/release/0.4.2/images/notification.jpg -------------------------------------------------------------------------------- /app/ios/build/Runner.build/Debug-iphonesimulator/Runner.build/dgph: -------------------------------------------------------------------------------- 1 | DGPH1.04 Oct 21 202221:59:43/UsersedwardcodeBangumiNappios -------------------------------------------------------------------------------- /app/document/release/0.3.1/images/launch_switch.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/document/release/0.3.1/images/launch_switch.jpg -------------------------------------------------------------------------------- /app/ios/build/Runner.build/Release-production-iphoneos/Runner.build/dgph: -------------------------------------------------------------------------------- 1 | DGPH1.04 Oct 21 202221:59:43/UsersedwardcodeBangumiNappios -------------------------------------------------------------------------------- /app/document/release/0.3.1/images/general_setting.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/document/release/0.3.1/images/general_setting.jpg -------------------------------------------------------------------------------- /app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/assets/logo/munin_logo_padding_rounded_2048x2048.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/assets/logo/munin_logo_padding_rounded_2048x2048.png -------------------------------------------------------------------------------- /app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/android/app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFF09199 4 | -------------------------------------------------------------------------------- /app/lib/config/.gitignore: -------------------------------------------------------------------------------- 1 | *.dart 2 | !application.dart 3 | !environment_example.dart 4 | !production_with_update_detector_example.dart 5 | !/upgrader/ 6 | !/upgrader/Utils.dart 7 | -------------------------------------------------------------------------------- /app/assets/stickers/readme.md: -------------------------------------------------------------------------------- 1 | ### Sticker assets 2 | 3 | Note that bangumi stickers have different naming extensions, changing file extensions 4 | will break code that reads stickers. -------------------------------------------------------------------------------- /app/ios/Flutter/Uat.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | 4 | FLUTTER_TARGET=lib/config/uat.dart 5 | -------------------------------------------------------------------------------- /app/lib/widgets/shared/common/Divider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | Divider onePixelHeightDivider() { 4 | return Divider( 5 | height: 1, 6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /app/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/ios/Runner/Assets.xcassets/LaunchImage.imageset/launch_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/launch_128x128.png -------------------------------------------------------------------------------- /app/ios/Runner/Assets.xcassets/LaunchImage.imageset/launch_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/launch_256x256.png -------------------------------------------------------------------------------- /app/ios/Runner/Assets.xcassets/LaunchImage.imageset/launch_384x384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/launch_384x384.png -------------------------------------------------------------------------------- /app/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | 4 | FLUTTER_TARGET=lib/config/development.dart 5 | -------------------------------------------------------------------------------- /app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /app/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | 4 | FLUTTER_TARGET=lib/config/production.dart 5 | -------------------------------------------------------------------------------- /app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardez/BangumiN/HEAD/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /app/android/app/src/main/java/com/bangumin/munin/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.bangumin.munin; 2 | 3 | import io.flutter.embedding.android.FlutterActivity; 4 | 5 | public class MainActivity extends FlutterActivity { 6 | } 7 | -------------------------------------------------------------------------------- /app/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/lib/providers/bangumi/readme.md: -------------------------------------------------------------------------------- 1 | # Html parser naming convention: 2 | 3 | * Expose top level method, all methods that parse sub element should be non `pubic` or 4 | `@VisibleForTesting` 5 | 6 | * Name top level method as `processXXXXXX` -------------------------------------------------------------------------------- /app/lib/models/bangumi/common/ChineseNameOwner.dart: -------------------------------------------------------------------------------- 1 | /// A mixin that can be used to indicate an entity has both an original 2 | /// name and a Chinese name. 3 | abstract class ChineseNameOwner { 4 | String get name; 5 | 6 | String get chineseName; 7 | } 8 | -------------------------------------------------------------------------------- /app/test/integration/http/readme.md: -------------------------------------------------------------------------------- 1 | # Http integration test 2 | 3 | The purpose of http integration test is to verify that munin can correctly makes http call. 4 | This is especially important for most cookie-based http call since we're mocking browser actions. -------------------------------------------------------------------------------- /app/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/lib/providers/README.md: -------------------------------------------------------------------------------- 1 | ### Intro 2 | 3 | A list of providers that provide required data by sending Http Request 4 | 5 | ### Credits 6 | 7 | We are parsing Bangumi WebPage to obtain some data, and some parsers are rewritten based on https://github.com/ekibun/Bangumi 8 | -------------------------------------------------------------------------------- /app/lib/config/production_with_update_detector_example.dart: -------------------------------------------------------------------------------- 1 | import 'package:munin/config/production.dart'; 2 | 3 | void main() => ExampleProductionWithUpdateDetector(); 4 | 5 | class ExampleProductionWithUpdateDetector extends Production { 6 | final shouldCheckUpdate = true; 7 | } 8 | -------------------------------------------------------------------------------- /app/lib/widgets/subject/info/SubjectInfoBottomSheet.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class SubjectInfoBottomSheet extends StatelessWidget { 4 | @override 5 | Widget build(BuildContext context) { 6 | // TODO: implement build 7 | return null; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /app/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Oct 03 01:00:15 PDT 2020 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip 7 | -------------------------------------------------------------------------------- /app/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /app/lib/widgets/initial/splash.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class InitialSplashPage extends StatelessWidget { 4 | @override 5 | Widget build(BuildContext context) { 6 | return Scaffold( 7 | body: Center( 8 | child: Text('加载中...'), 9 | ), 10 | ); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/lib/widgets/shared/utils/ExpandedEmpty.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | // an empty expanded widget that can be used to fill up space 4 | class ExpandedEmpty extends StatelessWidget { 5 | @override 6 | Widget build(BuildContext context) { 7 | return Expanded(child: Container()); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /app/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/lib/redux/setting/Common.dart: -------------------------------------------------------------------------------- 1 | /// [Screen.brightness] returns a value in range [0,1], it needs to be converted 2 | /// to a int percentage in range [0, 100] 3 | int roundDeviceBrightnessToPercentage(double deviceBrightness) { 4 | assert(deviceBrightness >= 0.0 && deviceBrightness <= 1.0); 5 | 6 | return (deviceBrightness * 100).round(); 7 | } 8 | -------------------------------------------------------------------------------- /app/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 8661d8aecd626f7f57ccbcb735553edc05a2e713 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /app/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/lib/config/environment_example.dart: -------------------------------------------------------------------------------- 1 | import 'package:munin/config/application.dart'; 2 | 3 | void main() => EnvironmentExample(); 4 | 5 | class EnvironmentExample extends Application { 6 | final environmentType = EnvironmentType.Development; 7 | final bangumiOauthClientIdentifier = '1'; 8 | final bangumiOauthClientSecret = '2'; 9 | final bangumiRedirectUrl = '3'; 10 | } 11 | -------------------------------------------------------------------------------- /app/lib/redux/shared/utils.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter_redux/flutter_redux.dart'; 3 | import 'package:munin/redux/app/AppState.dart'; 4 | import 'package:redux/redux.dart'; 5 | 6 | Store findStore(BuildContext context) => 7 | StoreProvider.of(context); 8 | 9 | AppState findAppState(BuildContext context) => findStore(context).state; 10 | -------------------------------------------------------------------------------- /app/lib/models/bangumi/timeline/common/HyperItem.dart: -------------------------------------------------------------------------------- 1 | import 'package:munin/models/bangumi/timeline/common/BangumiContent.dart'; 2 | 3 | /// A base interface for all BangumiContent types that have an id, a type and a page Url 4 | abstract class HyperItem { 5 | /// id of the hyper text 6 | String get id; 7 | 8 | /// content type 9 | BangumiContent get contentType; 10 | 11 | String get pageUrl; 12 | } 13 | -------------------------------------------------------------------------------- /app/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /app/lib/models/bangumi/mono/MonoBase.dart: -------------------------------------------------------------------------------- 1 | import 'package:built_value/built_value.dart'; 2 | 3 | part 'MonoBase.g.dart'; 4 | 5 | @BuiltValue(instantiable: false) 6 | abstract class MonoBase { 7 | int get id; 8 | 9 | @nullable 10 | @BuiltValueField(wireName: 'url') 11 | String get pageUrl; 12 | 13 | String get name; 14 | 15 | MonoBase rebuild(void updates(MonoBaseBuilder b)); 16 | 17 | MonoBaseBuilder toBuilder(); 18 | } 19 | -------------------------------------------------------------------------------- /app/test/Widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility that Flutter provides. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | 9 | 10 | void main() { 11 | } 12 | -------------------------------------------------------------------------------- /app/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/test/integration/http/config.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "bangumiOauthCredentials": { 3 | "accessToken": "", 4 | "refreshToken": "", 5 | "tokenEndpoint": "", 6 | "scopes": [], 7 | "expiration": 1 8 | }, 9 | "bangumiCookieCredentials": { 10 | "authCookie": "", 11 | "sessionCookie": "", 12 | "userAgent": "", 13 | "expiresOn": 1 14 | }, 15 | "bangumiOauthClientIdentifier": "", 16 | "bangumiOauthClientSecret": "" 17 | } -------------------------------------------------------------------------------- /app/lib/widgets/search/UserSearchResultWidget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:munin/models/bangumi/search/SearchRequest.dart'; 3 | 4 | class UserSearchResultWidget extends StatelessWidget { 5 | final SearchRequest searchRequest; 6 | 7 | const UserSearchResultWidget({Key key, @required this.searchRequest}) 8 | : super(key: key); 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return null; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/lib/widgets/setting/Common.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:munin/styles/theme/Common.dart'; 3 | import 'package:munin/widgets/shared/icons/AdaptiveIcons.dart'; 4 | 5 | Icon selectedOptionTrailingIcon(BuildContext context, {IconData iconData}) { 6 | if (iconData == null) { 7 | iconData = AdaptiveIcons.doneIconData; 8 | } 9 | 10 | return Icon( 11 | iconData, 12 | color: lightPrimaryDarkAccentColor(context), 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /app/ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/lib/styles/theme/CommonThemeData.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | BottomSheetThemeData muninBottomSheetThemeData({pureDartTheme = false}) { 4 | return BottomSheetThemeData( 5 | shape: RoundedRectangleBorder( 6 | borderRadius: BorderRadius.only( 7 | topLeft: Radius.circular(8.0), 8 | topRight: Radius.circular(8.0), 9 | ), 10 | ), 11 | elevation: 5.0, 12 | backgroundColor: pureDartTheme ? Colors.grey[850] : null, 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /app/lib/models/bangumi/timeline/common/FeedLoadType.dart: -------------------------------------------------------------------------------- 1 | enum FeedLoadType { 2 | /// feeds are currently empty, it's the initial load 3 | Initial, 4 | 5 | /// trying to load an older feed 6 | Older, 7 | 8 | /// trying to load feed between a gap(i.e. feed1,feed2....feed12,feed13, wants to load feed3-feed11), 9 | /// currently not in use, supporting Gap loading is non-trivial and might require 10 | /// Bangumi's official API 11 | Gap, 12 | 13 | /// trying to load a newer feed 14 | Newer, 15 | } 16 | -------------------------------------------------------------------------------- /app/lib/widgets/shared/utils/Scroll.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | 3 | void scrollPrimaryScrollControllerToTop(BuildContext context) { 4 | if (PrimaryScrollController.of(context) != null && 5 | PrimaryScrollController.of(context).hasClients) { 6 | /// eyeballed values from `_handleStatusBarTap` in `scaffold.dart` 7 | PrimaryScrollController.of(context).animateTo(0.0, 8 | duration: const Duration(milliseconds: 500), 9 | curve: Curves.linearToEaseOut); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/lib/shared/utils/a11y/common.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math' as math; 2 | import 'dart:ui'; 3 | 4 | /// Computes color ratio according to 5 | /// http://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef 6 | double computeContrastRatio( 7 | Color foreground, 8 | Color background, 9 | ) { 10 | final double lightness1 = foreground.computeLuminance() + 0.05; 11 | final double lightness2 = background.computeLuminance() + 0.05; 12 | return math.max(lightness1, lightness2) / math.min(lightness1, lightness2); 13 | } 14 | -------------------------------------------------------------------------------- /app/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /app/lib/widgets/setting/theme/Common.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:munin/widgets/setting/Common.dart'; 3 | import 'package:munin/widgets/shared/icons/AdaptiveIcons.dart'; 4 | 5 | Icon buildTrailingIcon(BuildContext context, T t1, T t2, 6 | {IconData iconData}) { 7 | if (iconData == null) { 8 | iconData = AdaptiveIcons.doneIconData; 9 | } 10 | if (t1 == t2) { 11 | return selectedOptionTrailingIcon(context, iconData: iconData); 12 | } else { 13 | return null; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/lib/widgets/shared/bottomsheet/showMinHeightModalBottomSheet.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | showMinHeightModalBottomSheet(BuildContext context, List children) { 4 | return showModalBottomSheet( 5 | context: context, 6 | builder: (innerContext) { 7 | return SafeArea( 8 | child: Column( 9 | mainAxisSize: MainAxisSize.min, 10 | crossAxisAlignment: CrossAxisAlignment.start, 11 | children: children, 12 | ), 13 | ); 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /app/android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() 4 | 5 | def plugins = new Properties() 6 | def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') 7 | if (pluginsFile.exists()) { 8 | pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } 9 | } 10 | 11 | plugins.each { name, path -> 12 | def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() 13 | include ":$name" 14 | project(":$name").projectDir = pluginDirectory 15 | } 16 | -------------------------------------------------------------------------------- /app/lib/models/bangumi/common/ItemMetaInfo.dart: -------------------------------------------------------------------------------- 1 | import 'package:built_value/built_value.dart'; 2 | import 'package:munin/models/bangumi/common/BangumiImage.dart'; 3 | 4 | /// a base TimelineFeed interface 5 | abstract class ItemMetaInfo { 6 | /// Updated at epoch time in milliseconds 7 | int get updatedAt; 8 | 9 | /// user nick name 10 | String get nickName; 11 | 12 | /// User avatars 13 | BangumiImage get avatar; 14 | 15 | /// user name, can only have digit and alphabetic 16 | String get username; 17 | 18 | @nullable 19 | String get actionName; 20 | } 21 | -------------------------------------------------------------------------------- /app/lib/widgets/shared/common/SingleChildExpandedRow.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | /// A widget that has a row layout and a single child inside a 4 | /// [Expanded]. 5 | class SingleChildExpandedRow extends StatelessWidget { 6 | final Widget child; 7 | 8 | const SingleChildExpandedRow({Key key, @required this.child}) 9 | : super(key: key); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return Row( 14 | children: [ 15 | Expanded( 16 | child: child, 17 | ) 18 | ], 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/lib/config/upgrader/readme.md: -------------------------------------------------------------------------------- 1 | Upgrade configuration for user who installed the app from a non-store environment. 2 | 3 | To indicate an update is a critical one in xml 4 | ```xml 5 | 6 | Version 0.3.3 7 | 修复崩溃 8 | 2019/07/11 20:00 9 | 10 | 11 | 12 | 14 | 15 | ``` -------------------------------------------------------------------------------- /app/lib/redux/oauth/OauthReducer.dart: -------------------------------------------------------------------------------- 1 | import 'package:munin/redux/oauth/OauthActions.dart'; 2 | import 'package:munin/redux/oauth/OauthState.dart'; 3 | import 'package:redux/redux.dart'; 4 | 5 | final oauthReducers = combineReducers([ 6 | TypedReducer(oAuthLoginFailureReducer), 7 | ]); 8 | 9 | // TODO: figure out whether it's needed to reset error message 10 | OauthState oAuthLoginFailureReducer( 11 | OauthState oauthState, OAuthLoginFailure oAuthLoginFailure) { 12 | return oauthState.rebuild((b) => b 13 | ..showLoginErrorSnackBar = true 14 | ..oauthFailureMessage = oAuthLoginFailure.message); 15 | } 16 | -------------------------------------------------------------------------------- /app/lib/models/bangumi/search/result/SearchResultItem.dart: -------------------------------------------------------------------------------- 1 | import 'package:built_value/built_value.dart'; 2 | import 'package:munin/models/bangumi/common/BangumiImage.dart'; 3 | import 'package:munin/models/bangumi/search/SearchType.dart'; 4 | 5 | part 'SearchResultItem.g.dart'; 6 | 7 | @BuiltValue(instantiable: false) 8 | abstract class SearchResultItem { 9 | @BuiltValueField(wireName: 'images') 10 | @nullable 11 | BangumiImage get image; 12 | 13 | String get name; 14 | 15 | int get id; 16 | 17 | SearchType get type; 18 | 19 | SearchResultItem rebuild(void updates(SearchResultItemBuilder b)); 20 | 21 | SearchResultItemBuilder toBuilder(); 22 | } 23 | -------------------------------------------------------------------------------- /app/lib/models/bangumi/progress/common/InProgressCollection.dart: -------------------------------------------------------------------------------- 1 | import 'package:built_value/built_value.dart'; 2 | import 'package:munin/models/bangumi/progress/common/InProgressSubjectInfo.dart'; 3 | 4 | part 'InProgressCollection.g.dart'; 5 | 6 | @BuiltValue(instantiable: false) 7 | abstract class InProgressCollection { 8 | /// The last time user touched this subject 9 | @BuiltValueField(wireName: 'lasttouch') 10 | int get userUpdatedAt; 11 | 12 | @BuiltValueField(wireName: 'subject') 13 | InProgressSubjectInfo get subject; 14 | 15 | InProgressCollection rebuild(void updates(InProgressCollectionBuilder b)); 16 | 17 | InProgressCollectionBuilder toBuilder(); 18 | } 19 | -------------------------------------------------------------------------------- /app/lib/models/bangumi/timeline/common/FeedMetaInfo.dart: -------------------------------------------------------------------------------- 1 | import 'package:built_value/built_value.dart'; 2 | import 'package:built_value/serializer.dart'; 3 | import 'package:munin/models/bangumi/common/BangumiImage.dart'; 4 | import 'package:munin/models/bangumi/common/ItemMetaInfo.dart'; 5 | 6 | part 'FeedMetaInfo.g.dart'; 7 | 8 | abstract class FeedMetaInfo 9 | implements ItemMetaInfo, Built { 10 | /// Bangumi feed id. 11 | int get feedId; 12 | 13 | FeedMetaInfo._(); 14 | 15 | factory FeedMetaInfo([updates(FeedMetaInfoBuilder b)]) = _$FeedMetaInfo; 16 | 17 | static Serializer get serializer => _$feedMetaInfoSerializer; 18 | } 19 | -------------------------------------------------------------------------------- /app/test/shared/utils/common_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:munin/shared/utils/common.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | group('firstNChars', () { 6 | test('Adds trailing string.', () { 7 | expect(firstNChars('12345', 4, trailingOverflowText: '...'), '1...'); 8 | }); 9 | 10 | test( 11 | 'Skips input if firstN is too small and trailingOverflowText is too large', 12 | () { 13 | expect(firstNChars('12345', 2, trailingOverflowText: '...'), '..'); 14 | }); 15 | 16 | test('Skips stripping input if firstN is longer than input', () { 17 | expect(firstNChars('12345', 100, trailingOverflowText: '...'), '12345'); 18 | }); 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /app/lib/models/bangumi/mono/Actor.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:built_value/built_value.dart'; 4 | import 'package:built_value/serializer.dart'; 5 | import 'package:munin/models/bangumi/mono/MonoBase.dart'; 6 | import 'package:munin/shared/utils/serializers.dart'; 7 | 8 | part 'Actor.g.dart'; 9 | 10 | abstract class Actor implements Built, MonoBase { 11 | Actor._(); 12 | 13 | factory Actor([updates(ActorBuilder b)]) = _$Actor; 14 | 15 | static Actor fromJson(String jsonString) { 16 | return serializers.deserializeWith( 17 | Actor.serializer, json.decode(jsonString)); 18 | } 19 | 20 | static Serializer get serializer => _$actorSerializer; 21 | } 22 | -------------------------------------------------------------------------------- /app/lib/models/bangumi/BangumiOauthCredentials.dart: -------------------------------------------------------------------------------- 1 | class BangumiOauthCredentials { 2 | // hard-code some pre-defined values for readability 3 | static final String tokenEndpoint = 'https://bgm.tv/oauth/access_token'; 4 | 5 | String accessToken; 6 | String refreshToken; 7 | int expiration; 8 | 9 | BangumiOauthCredentials.fromJson(Map json) 10 | : accessToken = json['accessToken'], 11 | refreshToken = json['accessToken'], 12 | expiration = json['accessToken']; 13 | 14 | Map toJson() => { 15 | 'accessToken': accessToken, 16 | 'refreshToken': refreshToken, 17 | 'expiration': expiration, 18 | 'tokenEndpoint': tokenEndpoint, 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /app/lib/widgets/shared/icons/MuninSmallTriangle.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:munin/widgets/shared/icons/MuninIcons.dart'; 3 | 4 | class MuninSmallTriangle extends StatelessWidget { 5 | /// Size of the icon, if unset, a special tiny size is used(see code). 6 | final double size; 7 | 8 | const MuninSmallTriangle({ 9 | Key key, 10 | this.size, 11 | }) : super(key: key); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return Icon( 16 | MuninIcons.muninRoundedTriangle, 17 | size: size ?? 18 | Theme.of(context).textTheme.subtitle1.fontSize * 19 | MediaQuery.of(context).textScaleFactor / 20 | 4.2, 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | 8 | # dependencies 9 | /node_modules 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | *.sublime-workspace 19 | 20 | # IDE - VSCode 21 | .vscode/* 22 | !.vscode/settings.json 23 | !.vscode/tasks.json 24 | !.vscode/launch.json 25 | !.vscode/extensions.json 26 | 27 | # misc 28 | /.sass-cache 29 | /connect.lock 30 | /coverage 31 | /libpeerconnection.log 32 | npm-debug.log 33 | testem.log 34 | /typings 35 | 36 | # e2e 37 | /e2e/*.js 38 | /e2e/*.map 39 | 40 | # System Files 41 | .DS_Store 42 | Thumbs.db 43 | 44 | /conf 45 | 46 | **/.ruby-version -------------------------------------------------------------------------------- /app/lib/models/bangumi/user/social/NetworkServiceTag.dart: -------------------------------------------------------------------------------- 1 | import 'package:built_value/built_value.dart'; 2 | import 'package:munin/models/bangumi/user/social/NetworkServiceType.dart'; 3 | 4 | part 'NetworkServiceTag.g.dart'; 5 | 6 | @BuiltValue(instantiable: false) 7 | abstract class NetworkServiceTag { 8 | NetworkServiceType get type; 9 | 10 | /// Content that's after this tag, this content is defined by user 11 | /// i.e. [PSN ID]: abcde 12 | /// Here content is abcde 13 | /// If the tag contains a hyperlink, then content is value of this hyperlink 14 | /// And the value is stored in [NetworkServiceTagLink.link] 15 | String get content; 16 | 17 | /// Whether value of the tag is a link or plain text 18 | bool get isLink; 19 | } 20 | -------------------------------------------------------------------------------- /app/lib/providers/bangumi/search/parser/isolate.dart: -------------------------------------------------------------------------------- 1 | import 'dart:collection'; 2 | 3 | import 'package:meta/meta.dart'; 4 | import 'package:munin/models/bangumi/search/SearchType.dart'; 5 | import 'package:munin/models/bangumi/search/result/MonoSearchResult.dart'; 6 | import 'package:munin/providers/bangumi/search/parser/MonoSearchParser.dart'; 7 | 8 | LinkedHashMap processMonoSearch( 9 | ParseMonoSearchMessage message) { 10 | return MonoSearchParser() 11 | .processMonoSearch(message.html, searchType: message.searchType); 12 | } 13 | 14 | class ParseMonoSearchMessage { 15 | final String html; 16 | 17 | final SearchType searchType; 18 | 19 | const ParseMonoSearchMessage(this.html, {@required this.searchType}); 20 | } 21 | -------------------------------------------------------------------------------- /app/lib/models/bangumi/user/notification/BaseNotificationItem.dart: -------------------------------------------------------------------------------- 1 | import 'package:built_value/built_value.dart'; 2 | import 'package:munin/models/bangumi/common/BangumiUserBasic.dart'; 3 | 4 | part 'BaseNotificationItem.g.dart'; 5 | 6 | @BuiltValue(instantiable: false) 7 | abstract class BaseNotificationItem { 8 | /// ID of the notification item. 9 | int get id; 10 | 11 | /// User who initiated this notification. 12 | BangumiUserBasic get initiator; 13 | 14 | /// Body content of the notification in html, initiator is not included. 15 | /// For example: "在你的日志 test 中发表了新回复" 16 | String get bodyContentHtml; 17 | 18 | BaseNotificationItem rebuild(void updates(BaseNotificationItemBuilder b)); 19 | 20 | BaseNotificationItemBuilder toBuilder(); 21 | } 22 | -------------------------------------------------------------------------------- /app/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.6.10' 3 | 4 | repositories { 5 | google() 6 | jcenter() 7 | } 8 | 9 | dependencies { 10 | classpath 'com.android.tools.build:gradle:4.2.2' 11 | classpath 'com.google.gms:google-services:4.3.3' 12 | classpath 'com.google.firebase:firebase-crashlytics-gradle:2.2.0' 13 | } 14 | } 15 | 16 | allprojects { 17 | repositories { 18 | google() 19 | jcenter() 20 | } 21 | } 22 | 23 | rootProject.buildDir = '../build' 24 | subprojects { 25 | project.buildDir = "${rootProject.buildDir}/${project.name}" 26 | } 27 | subprojects { 28 | project.evaluationDependsOn(':app') 29 | } 30 | 31 | task clean(type: Delete) { 32 | delete rootProject.buildDir 33 | } 34 | -------------------------------------------------------------------------------- /app/lib/models/bangumi/discussion/enums/DiscussionType.dart: -------------------------------------------------------------------------------- 1 | import 'package:built_collection/built_collection.dart'; 2 | import 'package:built_value/built_value.dart'; 3 | import 'package:built_value/serializer.dart'; 4 | 5 | part 'DiscussionType.g.dart'; 6 | 7 | class DiscussionType extends EnumClass { 8 | /// web: https://bgm.tv/rakuen/topiclist 9 | static const DiscussionType Rakuen = _$Rakuen; 10 | 11 | /// web: https://bgm.tv/group 12 | static const DiscussionType Group = _$Group; 13 | 14 | const DiscussionType._(String name) : super(name); 15 | 16 | static BuiltSet get values => _$values; 17 | 18 | static DiscussionType valueOf(String name) => _$valueOf(name); 19 | 20 | static Serializer get serializer => 21 | _$discussionTypeSerializer; 22 | } 23 | -------------------------------------------------------------------------------- /app/lib/models/bangumi/timeline/common/TimelineFeed.dart: -------------------------------------------------------------------------------- 1 | import 'package:built_value/built_value.dart'; 2 | import 'package:munin/models/bangumi/timeline/common/BangumiContent.dart'; 3 | import 'package:munin/models/bangumi/timeline/common/FeedMetaInfo.dart'; 4 | 5 | part 'TimelineFeed.g.dart'; 6 | 7 | /// a base TimelineFeed interface 8 | @BuiltValue(instantiable: false) 9 | abstract class TimelineFeed { 10 | FeedMetaInfo get user; 11 | 12 | BangumiContent get bangumiContent; 13 | 14 | /// To keep a complete timeline, muted user feeds will still be stored 15 | /// but not shown to user, this is controlled by `isFromMutedUser` field 16 | @nullable 17 | bool get isFromMutedUser; 18 | 19 | TimelineFeed rebuild(void updates(TimelineFeedBuilder b)); 20 | 21 | TimelineFeedBuilder toBuilder(); 22 | } 23 | -------------------------------------------------------------------------------- /app/lib/widgets/setting/mute/HowToAdd.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/widgets.dart'; 3 | import 'package:munin/widgets/shared/dialog/common.dart'; 4 | 5 | showHowToAddUserDialog(BuildContext context) { 6 | showMuninSingleActionDialog(context, 7 | title: Text('如何添加想屏蔽的用户'), 8 | content: Text('在app内前往用户主页,点击"屏蔽用户"按钮进行添加。' 9 | '你也可以点击屏蔽页面下方' 10 | '的"导入已绝交用户"来导入已绝交的用户。\n' 11 | '由于Bangumi的限制,目前暂时无法以搜索用户名的方式进行添加。\n\n' 12 | '屏蔽作用于除通知以外的地方(Bangumi已经提供了可以屏蔽通知的"绝交"功能。)')); 13 | } 14 | 15 | showHowToAddGroupDialog(BuildContext context) { 16 | showMuninSingleActionDialog(context, 17 | title: Text('如何添加想屏蔽的小组'), 18 | content: Text('在app内长按帖子图标会弹出"屏蔽此小组"选项,选择后即可屏蔽。\n' 19 | '由于Bangumi的限制,目前暂时无法以搜索小组名的方式进行添加。')); 20 | } 21 | -------------------------------------------------------------------------------- /app/lib/providers/bangumi/util/DioInterceptors.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:munin/shared/exceptions/exceptions.dart'; 3 | 4 | class BangumiCookieExpirationCheckInterceptor extends InterceptorsWrapper { 5 | /// [DateTime] that current cookie should expire. 6 | DateTime _expiresOn; 7 | 8 | set expiresOn(DateTime expiresOn) { 9 | _expiresOn = expiresOn; 10 | } 11 | 12 | BangumiCookieExpirationCheckInterceptor({DateTime expiresOn}) { 13 | this._expiresOn = expiresOn; 14 | } 15 | 16 | @override 17 | void onRequest(RequestOptions options, RequestInterceptorHandler handler) { 18 | if (_expiresOn != null && DateTime.now().isAfter(_expiresOn)) { 19 | throw AuthenticationExpiredException('认证已过期'); 20 | } 21 | 22 | return super.onRequest(options, handler); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/lib/widgets/shared/button/customization.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | /// A theme that specifies a small button 4 | /// Note: it doesn't align with Material standard spec and should be used 5 | /// as less as possible 6 | /// If height is not supplied, default value is twice the height of body1 fontSize 7 | ButtonThemeData smallButtonTheme( 8 | BuildContext context, { 9 | double minWidth = 48.0, 10 | double height, 11 | padding: const EdgeInsets.all(0), 12 | }) { 13 | ButtonThemeData defaultButtonTheme = Theme.of(context).buttonTheme; 14 | 15 | ButtonThemeData modifiedButtonTheme = defaultButtonTheme.copyWith( 16 | minWidth: minWidth, 17 | height: height ?? Theme.of(context).textTheme.bodyText2.fontSize * 2, 18 | padding: padding, 19 | ); 20 | 21 | return modifiedButtonTheme; 22 | } 23 | -------------------------------------------------------------------------------- /app/lib/shared/utils/analytics/Constants.dart: -------------------------------------------------------------------------------- 1 | class LoginElapsedTimeEvent { 2 | static const name = 'login_elapsed_time'; 3 | static const afterPostLoginCredentials = 4 | 'after_post_login_credentials_milliseconds'; 5 | static const afterPostOauthCredentials = 6 | 'after_post_oauth_credentials_milliseconds'; 7 | static const afterGetUserProfile = 'after_get_user_profile_milliseconds'; 8 | static const totalMilliSeconds = 'total_millisecondss'; 9 | } 10 | 11 | class LoginErrorEvent { 12 | static const name = 'login_error'; 13 | } 14 | 15 | class InstallUpdatePromptEvent { 16 | static const name = 'install_critical_update_prompt'; 17 | static const agreeToInstall = 'agree_to_install'; 18 | static const refuseToInstall = 'refuse_to_install'; 19 | static const criticalUpdateVersion = 'critical_update_version'; 20 | } 21 | -------------------------------------------------------------------------------- /app/lib/providers/bangumi/util/parser/ParseBangumiUser.dart: -------------------------------------------------------------------------------- 1 | import 'package:html/dom.dart'; 2 | import 'package:munin/models/bangumi/common/BangumiImage.dart'; 3 | import 'package:munin/models/bangumi/common/BangumiUserBasic.dart'; 4 | import 'package:munin/providers/bangumi/util/utils.dart'; 5 | 6 | BangumiUserBasic parseBangumiUserBasic(Element element) { 7 | final imageUrl = imageUrlFromBackgroundImage(element); 8 | 9 | final userNameElement = element.querySelector('a[href*="/user/"].l'); 10 | final nickname = userNameElement.text; 11 | 12 | final username = parseHrefId(userNameElement); 13 | return BangumiUserBasic( 14 | (b) => b 15 | ..nickname = nickname 16 | ..username = username 17 | ..avatar.replace(BangumiImage.fromImageUrl( 18 | imageUrl, ImageSize.Unknown, ImageType.UserAvatar)), 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /app/lib/widgets/shared/text/editor/showBangumiStickersBottomSheet.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:munin/widgets/shared/common/MuninPadding.dart'; 3 | import 'package:munin/widgets/shared/text/editor/sticker/BangumiStickers.dart'; 4 | 5 | showBangumiStickersBottomSheet( 6 | BuildContext context, Function(int id) onStickerTapped) { 7 | return showModalBottomSheet( 8 | context: context, 9 | builder: (innerContext) { 10 | return ListView( 11 | children: [ 12 | ListTile( 13 | title: Text('插入一个表情'), 14 | ), 15 | MuninPadding.vertical1xOffset( 16 | child: BangumiStickers(onStickerTapped: onStickerTapped), 17 | denseHorizontal: true, 18 | ) 19 | ], 20 | ); 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /app/lib/models/bangumi/subject/review/ReviewMetaInfo.dart: -------------------------------------------------------------------------------- 1 | import 'package:built_value/built_value.dart'; 2 | import 'package:built_value/serializer.dart'; 3 | import 'package:munin/models/bangumi/collection/CollectionStatus.dart'; 4 | import 'package:munin/models/bangumi/common/BangumiImage.dart'; 5 | import 'package:munin/models/bangumi/common/ItemMetaInfo.dart'; 6 | 7 | part 'ReviewMetaInfo.g.dart'; 8 | 9 | abstract class ReviewMetaInfo 10 | implements ItemMetaInfo, Built { 11 | @nullable 12 | double get score; 13 | 14 | @nullable 15 | CollectionStatus get collectionStatus; 16 | 17 | ReviewMetaInfo._(); 18 | 19 | factory ReviewMetaInfo([updates(ReviewMetaInfoBuilder b)]) = _$ReviewMetaInfo; 20 | 21 | static Serializer get serializer => 22 | _$reviewMetaInfoSerializer; 23 | } 24 | -------------------------------------------------------------------------------- /app/lib/providers/bangumi/util/parser/Spoiler.dart: -------------------------------------------------------------------------------- 1 | import 'package:html/dom.dart'; 2 | import 'package:munin/shared/utils/common.dart'; 3 | 4 | /// Removes bangumi spoiler text style then adds a 5 | /// [MuninCustomHtmlClasses.muninSpoiler] to it. 6 | /// 7 | /// Bangumi defines spoiler as a span that has `background-color:#555`, then 8 | /// uses js event listener to add hover effect, it won't work for us since we 9 | /// don't have event listener, hence this workaround is needed. 10 | DocumentFragment addSpoilerAttribute( 11 | DocumentFragment document, 12 | ) { 13 | final elements = 14 | document.querySelectorAll('span[style^="background-color:#555"]'); 15 | for (var element in elements) { 16 | element.attributes.remove('style'); 17 | element.classes.add(MuninCustomHtmlClasses.muninSpoiler); 18 | } 19 | 20 | return document; 21 | } 22 | -------------------------------------------------------------------------------- /app/lib/widgets/shared/button/FilledFlatButton.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:munin/styles/theme/Common.dart'; 3 | 4 | class FilledFlatButton extends StatelessWidget { 5 | final VoidCallback onPressed; 6 | 7 | final Widget child; 8 | 9 | const FilledFlatButton({ 10 | Key key, 11 | @required this.onPressed, 12 | this.child, 13 | }) : super(key: key); 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | return TextButton( 18 | style: TextButton.styleFrom( 19 | foregroundColor: Colors.white, 20 | backgroundColor: lightPrimaryDarkAccentColor(context), 21 | disabledBackgroundColor: 22 | lightPrimaryDarkAccentColor(context).withOpacity(0.1), 23 | ), 24 | onPressed: this.onPressed, 25 | child: child, 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/lib/models/bangumi/subject/common/SujectBase.dart: -------------------------------------------------------------------------------- 1 | import 'package:built_value/built_value.dart'; 2 | import 'package:munin/models/bangumi/common/ChineseNameOwner.dart'; 3 | 4 | part 'SujectBase.g.dart'; 5 | 6 | @BuiltValue(instantiable: false) 7 | abstract class SubjectBase with ChineseNameOwner { 8 | @nullable 9 | @BuiltValueField(wireName: 'id') 10 | int get id; 11 | 12 | /// This value is from Bangumi API, it might be null if data is obtained by 13 | /// html parser. And even api might not return this value. 14 | @nullable 15 | @BuiltValueField(wireName: 'url') 16 | String get pageUrlFromApi; 17 | 18 | String get name; 19 | 20 | @nullable 21 | @BuiltValueField(wireName: 'name_cn') 22 | String get chineseName; 23 | 24 | SubjectBase rebuild(void updates(SubjectBaseBuilder b)); 25 | 26 | SubjectBaseBuilder toBuilder(); 27 | } 28 | -------------------------------------------------------------------------------- /app/lib/widgets/shared/text/editor/sticker/utils.dart: -------------------------------------------------------------------------------- 1 | /// Utils for calculating sticker attributes based on sticker id. 2 | 3 | /// Gets sticker file extension. 4 | /// The first 22 stickers(except 11) are png, others are gif. 5 | String _idToStickerExtension(int id) { 6 | if (id <= 22 && id != 11) { 7 | return 'png'; 8 | } 9 | 10 | return 'gif'; 11 | } 12 | 13 | /// Bangumi names all stickers with id<10 as '01', '02'.. hence they need a 14 | /// padding, otherwise returns its original number in string format. 15 | String _idToBangumiName(int id) { 16 | return '${id.toString().padLeft(2, '0')}'; 17 | } 18 | 19 | String idToFullFileName(int id) { 20 | return '${_idToBangumiName(id)}.${_idToStickerExtension(id)}'; 21 | } 22 | 23 | /// Sticker code: (bgm01) ... (bgm23) 24 | String idToBangumiStickerCode(int id) { 25 | return '(bgm${_idToBangumiName(id)})'; 26 | } 27 | -------------------------------------------------------------------------------- /app/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 14.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/lib/config/upgrader/Utils.dart: -------------------------------------------------------------------------------- 1 | import 'package:upgrader/upgrader.dart'; 2 | 3 | appCastToString(AppcastItem appCastItem) { 4 | return 'AppcastItem{title: ${appCastItem.title}, \n' 5 | 'dateString: ${appCastItem.dateString}, \n' 6 | 'itemDescription: ${appCastItem.itemDescription}, \n' 7 | 'releaseNotesURL: ${appCastItem.releaseNotesURL}, \n' 8 | 'minimumSystemVersion: ${appCastItem.minimumSystemVersion}, \n' 9 | 'maximumSystemVersion: ${appCastItem.maximumSystemVersion}, \n' 10 | 'fileURL: ${appCastItem.fileURL}, \n' 11 | 'contentLength: ${appCastItem.contentLength}, \n' 12 | 'versionString: ${appCastItem.versionString}, \n' 13 | 'osString: ${appCastItem.osString}, \n' 14 | 'displayVersionString: ${appCastItem.displayVersionString}, \n' 15 | 'infoURL: ${appCastItem.infoURL}, \n' 16 | 'tags: ${appCastItem.tags}}'; 17 | } 18 | -------------------------------------------------------------------------------- /app/ios/Runner/Assets.xcassets/SplashScreenBackgroundColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "version": 1, 4 | "author": "xcode" 5 | }, 6 | "colors": [ 7 | { 8 | "idiom": "universal", 9 | "color": { 10 | "color-space": "srgb", 11 | "components": { 12 | "red": "1.000", 13 | "alpha": "1.000", 14 | "blue": "1.000", 15 | "green": "1.000" 16 | } 17 | } 18 | }, 19 | { 20 | "idiom": "universal", 21 | "appearances": [ 22 | { 23 | "appearance": "luminosity", 24 | "value": "dark" 25 | } 26 | ], 27 | "color": { 28 | "color-space": "srgb", 29 | "components": { 30 | "red": "0.000", 31 | "alpha": "1.000", 32 | "blue": "0.000", 33 | "green": "0.000" 34 | } 35 | } 36 | } 37 | ] 38 | } -------------------------------------------------------------------------------- /app/lib/widgets/shared/common/HorizontalScrollableWidget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class HorizontalScrollableWidget extends StatelessWidget { 4 | final List horizontalList; 5 | 6 | final double listHeight; 7 | 8 | final ScrollPhysics physics; 9 | 10 | const HorizontalScrollableWidget({ 11 | Key key, 12 | @required this.horizontalList, 13 | @required this.listHeight, 14 | this.physics, 15 | }) : super(key: key); 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | return Container( 20 | height: listHeight, 21 | child: ListView.builder( 22 | physics: physics, 23 | scrollDirection: Axis.horizontal, 24 | itemBuilder: (BuildContext context, index) { 25 | return horizontalList[index]; 26 | }, 27 | itemCount: horizontalList.length, 28 | ), 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/lib/widgets/home/HomePageAppBarTitle.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:munin/widgets/shared/button/FlatButtonWithTrailingIcon.dart'; 3 | import 'package:munin/widgets/shared/icons/MuninSmallTriangle.dart'; 4 | 5 | /// A common appbar title displayed across all home screens. 6 | class HomePageAppBarTitle extends StatelessWidget { 7 | final String titleText; 8 | 9 | /// The callback that is called when the button is tapped or otherwise activated. 10 | final VoidCallback onPressed; 11 | 12 | const HomePageAppBarTitle({ 13 | Key key, 14 | @required this.titleText, 15 | @required this.onPressed, 16 | }) : super(key: key); 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | return FlatButtonWithTrailingIcon( 21 | onPressed: onPressed, 22 | label: Text(titleText), 23 | icon: MuninSmallTriangle(), 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/lib/shared/utils/http/common.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io' show ContentType; 2 | 3 | /// Determines whether the status code is a canonical http status 2xx code 4 | bool is2xxCode(int code) { 5 | return code != null && code >= 200 && code < 300; 6 | } 7 | 8 | /// Bangumi sometimes doesn't use regular http status code 9 | /// Instead it always returns http status code 200, and in the json there is a 10 | /// `code` field which contains the actual status code 11 | /// if this `code` field is null, we assume it also means successful 12 | bool isBangumi2xxCode(int code) { 13 | return code != null && code >= 200 && code < 300; 14 | } 15 | 16 | bool isBangumiWebPageOkResponse(dynamic decodedResponse) { 17 | return decodedResponse is Map && decodedResponse['status'] == 'ok'; 18 | } 19 | 20 | class ExtraContentType { 21 | static final xWwwFormUrlencoded = 22 | ContentType.parse("application/x-www-form-urlencoded"); 23 | } 24 | -------------------------------------------------------------------------------- /app/lib/shared/utils/ui/completers.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:munin/widgets/shared/common/SnackBar.dart'; 5 | 6 | /// a completer helper util to show snack bar with a error dialog if this completer fails 7 | /// Note: even shouldPop is set to true, pop may not happen since `maybePop` is used 8 | Completer snackBarCompleter(BuildContext context, String message, 9 | {bool shouldPop = false}) { 10 | final Completer completer = Completer(); 11 | 12 | completer.future.then((_) { 13 | if (shouldPop) { 14 | Navigator.of(context).maybePop(); 15 | } 16 | showTextOnSnackBar(context, message); 17 | }).catchError((Object error) { 18 | showDialog( 19 | context: context, 20 | builder: (BuildContext context) { 21 | return error; 22 | }); 23 | }); 24 | 25 | return completer; 26 | } 27 | -------------------------------------------------------------------------------- /app/lib/models/bangumi/search/result/BangumiSearchResponse.dart: -------------------------------------------------------------------------------- 1 | import 'package:built_collection/built_collection.dart'; 2 | import 'package:built_value/built_value.dart'; 3 | import 'package:munin/models/bangumi/search/result/SearchResultItem.dart'; 4 | 5 | part 'BangumiSearchResponse.g.dart'; 6 | 7 | @BuiltValue(instantiable: false) 8 | abstract class BangumiSearchResponse { 9 | int get totalCount; 10 | 11 | @nullable 12 | int get requestedResults; 13 | 14 | @nullable 15 | BuiltMap get results; 16 | 17 | /// seems like there is no better way in built_value to specify a getter 18 | /// in interface 19 | @memoized 20 | bool get hasReachedEnd { 21 | throw UnsupportedError('Sub concrete class need to implement this getter'); 22 | } 23 | 24 | BangumiSearchResponse rebuild(void updates(BangumiSearchResponseBuilder b)); 25 | 26 | BangumiSearchResponseBuilder toBuilder(); 27 | } 28 | -------------------------------------------------------------------------------- /app/lib/redux/app/AppActions.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:munin/redux/app/AppState.dart'; 3 | import 'package:munin/redux/app/BasicAppState.dart'; 4 | 5 | class PersistAppStateAction { 6 | /// Whether only [BasicAppState] should be persisted 7 | final bool basicAppStateOnly; 8 | 9 | /// An optional pass-ed in [AppState]. If not set, current [AppState] in 10 | /// [Store] will be used. 11 | final AppState appState; 12 | 13 | PersistAppStateAction({ 14 | this.basicAppStateOnly = false, 15 | this.appState, 16 | }); 17 | } 18 | 19 | class HandleErrorAction { 20 | final BuildContext context; 21 | final Object error; 22 | final StackTrace stack; 23 | 24 | final bool showErrorMessageSnackBar; 25 | 26 | HandleErrorAction({ 27 | @required this.context, 28 | @required this.error, 29 | this.stack, 30 | this.showErrorMessageSnackBar = true, 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /app/lib/shared/utils/misc/async.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:munin/shared/exceptions/utils.dart'; 4 | 5 | Completer immediateFinishCompleter() { 6 | Completer completer = Completer(); 7 | completer.complete(); 8 | return completer; 9 | } 10 | 11 | /// Completes [completer] if it's not completed it. 12 | /// 13 | /// It's useful as a sanity check to complete dangling completer since Calling 14 | /// [Completer.complete] or [Completer.completeError] must be done at most once. 15 | completeDanglingCompleter(Completer completer) { 16 | if (completer != null && !completer.isCompleted) { 17 | completer.complete(); 18 | } 19 | } 20 | 21 | completeWithErrorAndReport(error, 22 | Completer completer, { 23 | StackTrace stack, 24 | }) { 25 | print(error.toString()); 26 | print(stack); 27 | reportError(error, stack: stack); 28 | completer.completeError(error, stack); 29 | } 30 | -------------------------------------------------------------------------------- /app/lib/models/bangumi/subject/common/SubjectStatus.dart: -------------------------------------------------------------------------------- 1 | import 'package:built_collection/built_collection.dart'; 2 | import 'package:built_value/built_value.dart'; 3 | import 'package:built_value/serializer.dart'; 4 | 5 | part 'SubjectStatus.g.dart'; 6 | 7 | /// Status of a subject on bangumi. 8 | class SubjectStatus extends EnumClass { 9 | static const SubjectStatus Unknown = _$Unknown; 10 | 11 | static const SubjectStatus Normal = _$Normal; 12 | 13 | /// Subject is locked. User is not allowed to modify collection status 14 | /// for this subject. 15 | static const SubjectStatus LockedForCollection = _$LockedForCollection; 16 | 17 | const SubjectStatus._(String name) : super(name); 18 | 19 | static BuiltSet get values => _$values; 20 | 21 | static SubjectStatus valueOf(String name) => _$valueOf(name); 22 | 23 | static Serializer get serializer => _$subjectStatusSerializer; 24 | } 25 | -------------------------------------------------------------------------------- /app/lib/models/bangumi/timeline/WikiCreationSingle.dart: -------------------------------------------------------------------------------- 1 | import 'package:built_value/built_value.dart'; 2 | import 'package:built_value/serializer.dart'; 3 | import 'package:munin/models/bangumi/timeline/common/BangumiContent.dart'; 4 | import 'package:munin/models/bangumi/timeline/common/FeedMetaInfo.dart'; 5 | import 'package:munin/models/bangumi/timeline/common/TimelineFeed.dart'; 6 | 7 | part 'WikiCreationSingle.g.dart'; 8 | 9 | abstract class WikiCreationSingle 10 | implements 11 | Built, 12 | TimelineFeed { 13 | FeedMetaInfo get user; 14 | 15 | String get newItemName; 16 | 17 | String get newItemId; 18 | 19 | WikiCreationSingle._(); 20 | 21 | factory WikiCreationSingle([updates(WikiCreationSingleBuilder b)]) = 22 | _$WikiCreationSingle; 23 | 24 | static Serializer get serializer => 25 | _$wikiCreationSingleSerializer; 26 | } 27 | -------------------------------------------------------------------------------- /app/lib/widgets/shared/background/GreyRoundedBorderContainer.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:munin/styles/theme/Common.dart'; 3 | 4 | class GreyRoundedBorderContainer extends StatelessWidget { 5 | final Widget child; 6 | 7 | /// The amount of space by which to inset the child. 8 | final EdgeInsetsGeometry childPadding; 9 | 10 | const GreyRoundedBorderContainer({ 11 | Key key, 12 | @required this.child, 13 | this.childPadding = const EdgeInsets.all(baseOffset), 14 | }) : super(key: key); 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return Container( 19 | decoration: BoxDecoration( 20 | border: Border.all(color: Theme.of(context).textTheme.caption.color), 21 | borderRadius: defaultContainerCircularRadius), 22 | child: Padding( 23 | child: child, 24 | padding: childPadding, 25 | ), 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/lib/widgets/user/UserHome.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:munin/models/bangumi/setting/general/PreferredLaunchNavTab.dart'; 3 | import 'package:munin/redux/shared/utils.dart'; 4 | import 'package:munin/widgets/shared/appbar/OneMuninBar.dart'; 5 | 6 | import 'UserProfileWidget.dart'; 7 | 8 | class UserHome extends StatelessWidget { 9 | const UserHome({Key key}) : super(key: key); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return UserProfileWidget( 14 | username: findAppState(context).currentAuthenticatedUserBasicInfo 15 | .username, 16 | appBar: OneMuninBar( 17 | title: Text( 18 | PreferredLaunchNavTab.HomePage.generalSettingPageChineseName, 19 | style: Theme 20 | .of(context) 21 | .textTheme 22 | .button, 23 | ), 24 | addNotificationWidget: true, 25 | ), 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/lib/models/bangumi/mono/MonoBase.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'MonoBase.dart'; 4 | 5 | // ************************************************************************** 6 | // BuiltValueGenerator 7 | // ************************************************************************** 8 | 9 | abstract class MonoBaseBuilder { 10 | void replace(MonoBase other); 11 | void update(void Function(MonoBaseBuilder) updates); 12 | int get id; 13 | set id(int id); 14 | 15 | String get pageUrl; 16 | set pageUrl(String pageUrl); 17 | 18 | String get name; 19 | set name(String name); 20 | } 21 | 22 | // ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new 23 | -------------------------------------------------------------------------------- /app/lib/providers/bangumi/progress/parser/isolate.dart: -------------------------------------------------------------------------------- 1 | import 'dart:collection'; 2 | 3 | import 'package:munin/models/bangumi/progress/api/EpisodeProgress.dart'; 4 | import 'package:munin/models/bangumi/progress/common/EpisodeStatus.dart'; 5 | import 'package:munin/models/bangumi/progress/html/SubjectEpisodes.dart'; 6 | import 'package:munin/providers/bangumi/progress/parser/ProgressParser.dart'; 7 | 8 | LinkedHashMap> processProgressPreview( 9 | String rawHtml) { 10 | return ProgressParser().processProgressPreview(rawHtml); 11 | } 12 | 13 | SubjectEpisodes processSubjectEpisodes(ParseSubjectEpisodesMessage message) { 14 | return ProgressParser() 15 | .processSubjectEpisodes(message.html, message.touchedEpisodes); 16 | } 17 | 18 | class ParseSubjectEpisodesMessage { 19 | final String html; 20 | 21 | final Map touchedEpisodes; 22 | 23 | const ParseSubjectEpisodesMessage(this.html, this.touchedEpisodes); 24 | } 25 | -------------------------------------------------------------------------------- /app/lib/styles/theme/NightDeepGreyBlue.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:munin/styles/theme/Common.dart'; 3 | import 'package:munin/styles/theme/CommonThemeData.dart'; 4 | 5 | final ThemeData nightDeepGreyBlueThemeData = ThemeData( 6 | brightness: Brightness.dark, 7 | bottomSheetTheme: muninBottomSheetThemeData(), 8 | buttonTheme: ButtonThemeData( 9 | shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)), 10 | ), 11 | dialogTheme: DialogTheme( 12 | shape: RoundedRectangleBorder(borderRadius: defaultContainerCircularRadius), 13 | ), 14 | sliderTheme: SliderThemeData.fromPrimaryColors( 15 | primaryColor: Colors.lightBlueAccent, 16 | primaryColorDark: Colors.lightBlueAccent.shade700, 17 | valueIndicatorTextStyle: ThemeData().textTheme.bodyText1, 18 | primaryColorLight: Colors.lightBlueAccent.shade100, 19 | ), 20 | accentColor: Colors.lightBlueAccent, 21 | toggleableActiveColor: Colors.lightBlueAccent, 22 | ); 23 | -------------------------------------------------------------------------------- /app/lib/widgets/timeline/item/UnknownTimelineActivityWidget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:munin/models/bangumi/timeline/UnknownTimelineActivity.dart'; 3 | import 'package:munin/shared/utils/common.dart'; 4 | 5 | /// an unknown timeline activity, only truncated plain text will be shown 6 | class UnknownTimelineActivityWidget extends StatelessWidget { 7 | final UnknownTimelineActivity unknownTimelineActivity; 8 | 9 | final int maxUnknownTimelineActivityLength = 500; 10 | 11 | const UnknownTimelineActivityWidget( 12 | {Key key, @required this.unknownTimelineActivity}) 13 | : super(key: key); 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | return Row( 18 | children: [ 19 | Flexible( 20 | child: Text(firstNChars( 21 | unknownTimelineActivity.content, maxUnknownTimelineActivityLength, 22 | fallbackValue: null)), 23 | ) 24 | ], 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/lib/models/bangumi/search/SearchRequest.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:built_value/built_value.dart'; 4 | import 'package:built_value/serializer.dart'; 5 | import 'package:munin/models/bangumi/search/SearchType.dart'; 6 | import 'package:munin/shared/utils/serializers.dart'; 7 | 8 | part 'SearchRequest.g.dart'; 9 | 10 | abstract class SearchRequest 11 | implements Built { 12 | SearchRequest._(); 13 | 14 | String get query; 15 | 16 | SearchType get searchType; 17 | 18 | factory SearchRequest([updates(SearchRequestBuilder b)]) = _$SearchRequest; 19 | 20 | String toJson() { 21 | return json 22 | .encode(serializers.serializeWith(SearchRequest.serializer, this)); 23 | } 24 | 25 | static SearchRequest fromJson(String jsonString) { 26 | return serializers.deserializeWith( 27 | SearchRequest.serializer, json.decode(jsonString)); 28 | } 29 | 30 | static Serializer get serializer => _$searchRequestSerializer; 31 | } 32 | -------------------------------------------------------------------------------- /app/lib/redux/oauth/OauthState.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:built_value/built_value.dart'; 4 | import 'package:built_value/serializer.dart'; 5 | import 'package:munin/shared/utils/serializers.dart'; 6 | 7 | part 'OauthState.g.dart'; 8 | 9 | abstract class OauthState implements Built { 10 | @nullable 11 | bool get showLoginErrorSnackBar; 12 | 13 | @nullable 14 | String get oauthFailureMessage; 15 | 16 | @nullable 17 | String get error; 18 | 19 | OauthState._(); 20 | 21 | factory OauthState([updates(OauthStateBuilder b)]) => 22 | _$OauthState((b) => b..update(updates)); 23 | 24 | String toJson() { 25 | return json.encode(serializers.serializeWith(OauthState.serializer, this)); 26 | } 27 | 28 | static OauthState fromJson(String jsonString) { 29 | return serializers.deserializeWith( 30 | OauthState.serializer, json.decode(jsonString)); 31 | } 32 | 33 | static Serializer get serializer => _$oauthStateSerializer; 34 | } 35 | -------------------------------------------------------------------------------- /app/lib/models/bangumi/timeline/ProgressUpdateEpisodeUntil.dart: -------------------------------------------------------------------------------- 1 | import 'package:built_value/built_value.dart'; 2 | import 'package:built_value/serializer.dart'; 3 | import 'package:munin/models/bangumi/timeline/common/BangumiContent.dart'; 4 | import 'package:munin/models/bangumi/timeline/common/FeedMetaInfo.dart'; 5 | import 'package:munin/models/bangumi/timeline/common/TimelineFeed.dart'; 6 | 7 | part 'ProgressUpdateEpisodeUntil.g.dart'; 8 | 9 | abstract class ProgressUpdateEpisodeUntil 10 | implements 11 | Built, 12 | TimelineFeed { 13 | FeedMetaInfo get user; 14 | 15 | String get subjectName; 16 | 17 | String get subjectId; 18 | 19 | ProgressUpdateEpisodeUntil._(); 20 | 21 | factory ProgressUpdateEpisodeUntil( 22 | [updates(ProgressUpdateEpisodeUntilBuilder b)]) = 23 | _$ProgressUpdateEpisodeUntil; 24 | 25 | static Serializer get serializer => 26 | _$progressUpdateEpisodeUntilSerializer; 27 | } 28 | -------------------------------------------------------------------------------- /app/lib/widgets/shared/button/RoundedInkWell.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class RoundedInkWell extends StatelessWidget { 4 | final Widget child; 5 | 6 | final GestureTapCallback onTap; 7 | 8 | /// Text that describes the action that will occur when the button is pressed. 9 | /// 10 | /// This text is displayed when the user long-presses on the button and is 11 | /// used for accessibility. 12 | final String tooltip; 13 | 14 | const RoundedInkWell({ 15 | Key key, 16 | @required this.child, 17 | this.onTap, 18 | this.tooltip, 19 | }) : super(key: key); 20 | 21 | @override 22 | Widget build(BuildContext context) { 23 | final roundedInkWell = InkWell( 24 | child: child, 25 | borderRadius: BorderRadius.all(Radius.circular(2.0)), 26 | onTap: onTap, 27 | ); 28 | 29 | if (tooltip != null) { 30 | return Tooltip( 31 | child: roundedInkWell, 32 | message: tooltip, 33 | ); 34 | } 35 | 36 | return roundedInkWell; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/lib/models/bangumi/timeline/GroupJoinSingle.dart: -------------------------------------------------------------------------------- 1 | import 'package:built_value/built_value.dart'; 2 | import 'package:built_value/serializer.dart'; 3 | import 'package:munin/models/bangumi/common/BangumiImage.dart'; 4 | import 'package:munin/models/bangumi/timeline/common/BangumiContent.dart'; 5 | import 'package:munin/models/bangumi/timeline/common/FeedMetaInfo.dart'; 6 | import 'package:munin/models/bangumi/timeline/common/TimelineFeed.dart'; 7 | 8 | part 'GroupJoinSingle.g.dart'; 9 | 10 | abstract class GroupJoinSingle 11 | implements Built, TimelineFeed { 12 | FeedMetaInfo get user; 13 | 14 | BangumiImage get groupIcon; 15 | 16 | String get groupName; 17 | 18 | @nullable 19 | String get groupDescription; 20 | 21 | String get groupId; 22 | 23 | GroupJoinSingle._(); 24 | 25 | factory GroupJoinSingle([updates(GroupJoinSingleBuilder b)]) = 26 | _$GroupJoinSingle; 27 | 28 | static Serializer get serializer => 29 | _$groupJoinSingleSerializer; 30 | } 31 | -------------------------------------------------------------------------------- /app/lib/models/bangumi/timeline/BlogCreationSingle.dart: -------------------------------------------------------------------------------- 1 | import 'package:built_value/built_value.dart'; 2 | import 'package:built_value/serializer.dart'; 3 | import 'package:munin/models/bangumi/timeline/common/BangumiContent.dart'; 4 | import 'package:munin/models/bangumi/timeline/common/FeedMetaInfo.dart'; 5 | import 'package:munin/models/bangumi/timeline/common/TimelineFeed.dart'; 6 | 7 | part 'BlogCreationSingle.g.dart'; 8 | 9 | abstract class BlogCreationSingle 10 | implements 11 | Built, 12 | TimelineFeed { 13 | /// due to the limitation of bangumi, this has to be a string 14 | FeedMetaInfo get user; 15 | 16 | String get title; 17 | 18 | @nullable 19 | String get summary; 20 | 21 | /// blog id 22 | String get id; 23 | 24 | BlogCreationSingle._(); 25 | 26 | factory BlogCreationSingle([updates(BlogCreationSingleBuilder b)]) = 27 | _$BlogCreationSingle; 28 | 29 | static Serializer get serializer => 30 | _$blogCreationSingleSerializer; 31 | } 32 | -------------------------------------------------------------------------------- /app/lib/models/bangumi/progress/common/InProgressCollection.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'InProgressCollection.dart'; 4 | 5 | // ************************************************************************** 6 | // BuiltValueGenerator 7 | // ************************************************************************** 8 | 9 | abstract class InProgressCollectionBuilder { 10 | void replace(InProgressCollection other); 11 | void update(void Function(InProgressCollectionBuilder) updates); 12 | int get userUpdatedAt; 13 | set userUpdatedAt(int userUpdatedAt); 14 | 15 | InProgressSubjectInfoBuilder get subject; 16 | set subject(InProgressSubjectInfoBuilder subject); 17 | } 18 | 19 | // ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new 20 | -------------------------------------------------------------------------------- /app/lib/models/bangumi/timeline/PublicMessageNoReply.dart: -------------------------------------------------------------------------------- 1 | import 'package:built_value/built_value.dart'; 2 | import 'package:built_value/serializer.dart'; 3 | import 'package:munin/models/bangumi/timeline/common/BangumiContent.dart'; 4 | import 'package:munin/models/bangumi/timeline/common/FeedMetaInfo.dart'; 5 | import 'package:munin/models/bangumi/timeline/common/TimelineFeed.dart'; 6 | 7 | part 'PublicMessageNoReply.g.dart'; 8 | 9 | /// a special public message that's published by system and un-deletable 10 | abstract class PublicMessageNoReply 11 | implements 12 | Built, 13 | TimelineFeed { 14 | /// due to the limitation of bangumi, this has to be a string 15 | FeedMetaInfo get user; 16 | 17 | String get content; 18 | 19 | PublicMessageNoReply._(); 20 | 21 | factory PublicMessageNoReply([updates(PublicMessageNoReplyBuilder b)]) = 22 | _$PublicMessageNoReply; 23 | 24 | static Serializer get serializer => 25 | _$publicMessageNoReplySerializer; 26 | } 27 | -------------------------------------------------------------------------------- /app/lib/models/bangumi/timeline/common/HyperImage.dart: -------------------------------------------------------------------------------- 1 | import 'package:built_value/built_value.dart'; 2 | import 'package:built_value/serializer.dart'; 3 | import 'package:munin/models/bangumi/common/BangumiImage.dart'; 4 | import 'package:munin/models/bangumi/timeline/common/BangumiContent.dart'; 5 | import 'package:munin/models/bangumi/timeline/common/HyperItem.dart'; 6 | 7 | part 'HyperImage.g.dart'; 8 | 9 | /// HyperImage: image with a clickable link 10 | abstract class HyperImage 11 | implements Built, HyperItem { 12 | /// id of the hyper text 13 | String get id; 14 | 15 | /// content type 16 | BangumiContent get contentType; 17 | 18 | BangumiImage get image; 19 | 20 | /// if we cannot parse content, a fallback webview might be used 21 | /// hence an optional link is needed 22 | @nullable 23 | String get pageUrl; 24 | 25 | HyperImage._(); 26 | 27 | factory HyperImage([updates(HyperImageBuilder b)]) = _$HyperImage; 28 | 29 | static Serializer get serializer => _$hyperImageSerializer; 30 | } 31 | -------------------------------------------------------------------------------- /app/lib/models/bangumi/user/social/NetworkServiceTag.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'NetworkServiceTag.dart'; 4 | 5 | // ************************************************************************** 6 | // BuiltValueGenerator 7 | // ************************************************************************** 8 | 9 | abstract class NetworkServiceTagBuilder { 10 | void replace(NetworkServiceTag other); 11 | void update(void Function(NetworkServiceTagBuilder) updates); 12 | NetworkServiceType get type; 13 | set type(NetworkServiceType type); 14 | 15 | String get content; 16 | set content(String content); 17 | 18 | bool get isLink; 19 | set isLink(bool isLink); 20 | } 21 | 22 | // ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new 23 | -------------------------------------------------------------------------------- /app/lib/models/bangumi/subject/review/SubjectReview.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:built_value/built_value.dart'; 4 | import 'package:built_value/serializer.dart'; 5 | import 'package:munin/models/bangumi/subject/review/ReviewMetaInfo.dart'; 6 | import 'package:munin/shared/utils/serializers.dart'; 7 | 8 | part 'SubjectReview.g.dart'; 9 | 10 | abstract class SubjectReview 11 | implements Built { 12 | SubjectReview._(); 13 | 14 | factory SubjectReview([updates(SubjectReviewBuilder b)]) = _$SubjectReview; 15 | 16 | ReviewMetaInfo get metaInfo; 17 | 18 | @nullable 19 | String get content; 20 | 21 | String toJson() { 22 | return json 23 | .encode(serializers.serializeWith(SubjectReview.serializer, this)); 24 | } 25 | 26 | static SubjectReview fromJson(String jsonString) { 27 | return serializers.deserializeWith( 28 | SubjectReview.serializer, json.decode(jsonString)); 29 | } 30 | 31 | static Serializer get serializer => _$subjectReviewSerializer; 32 | } 33 | -------------------------------------------------------------------------------- /app/lib/models/bangumi/timeline/IndexFavoriteSingle.dart: -------------------------------------------------------------------------------- 1 | import 'package:built_value/built_value.dart'; 2 | import 'package:built_value/serializer.dart'; 3 | import 'package:munin/models/bangumi/timeline/common/BangumiContent.dart'; 4 | import 'package:munin/models/bangumi/timeline/common/FeedMetaInfo.dart'; 5 | import 'package:munin/models/bangumi/timeline/common/TimelineFeed.dart'; 6 | 7 | part 'IndexFavoriteSingle.g.dart'; 8 | 9 | abstract class IndexFavoriteSingle 10 | implements 11 | Built, 12 | TimelineFeed { 13 | /// due to the limitation of bangumi, this has to be a string 14 | FeedMetaInfo get user; 15 | 16 | String get title; 17 | 18 | /// id of the index 19 | String get id; 20 | 21 | @nullable 22 | String get summary; 23 | 24 | IndexFavoriteSingle._(); 25 | 26 | factory IndexFavoriteSingle([updates(IndexFavoriteSingleBuilder b)]) = 27 | _$IndexFavoriteSingle; 28 | 29 | static Serializer get serializer => 30 | _$indexFavoriteSingleSerializer; 31 | } 32 | -------------------------------------------------------------------------------- /app/lib/models/bangumi/timeline/UnknownTimelineActivity.dart: -------------------------------------------------------------------------------- 1 | import 'package:built_value/built_value.dart'; 2 | import 'package:built_value/serializer.dart'; 3 | import 'package:munin/models/bangumi/timeline/common/BangumiContent.dart'; 4 | import 'package:munin/models/bangumi/timeline/common/FeedMetaInfo.dart'; 5 | import 'package:munin/models/bangumi/timeline/common/TimelineFeed.dart'; 6 | 7 | part 'UnknownTimelineActivity.g.dart'; 8 | 9 | abstract class UnknownTimelineActivity 10 | implements 11 | Built, 12 | TimelineFeed { 13 | /// no-op, this should always be null 14 | @nullable 15 | FeedMetaInfo get user; 16 | 17 | String get content; 18 | 19 | @nullable 20 | BangumiContent get bangumiContent; 21 | 22 | UnknownTimelineActivity._(); 23 | 24 | factory UnknownTimelineActivity([updates(UnknownTimelineActivityBuilder b)]) = 25 | _$UnknownTimelineActivity; 26 | 27 | static Serializer get serializer => 28 | _$unknownTimelineActivitySerializer; 29 | } 30 | -------------------------------------------------------------------------------- /app/lib/models/bangumi/timeline/common/Mono.dart: -------------------------------------------------------------------------------- 1 | import 'package:built_collection/built_collection.dart'; 2 | import 'package:built_value/built_value.dart'; 3 | import 'package:built_value/serializer.dart'; 4 | import 'package:munin/models/bangumi/timeline/common/BangumiContent.dart'; 5 | 6 | part 'Mono.g.dart'; 7 | 8 | class Mono extends EnumClass { 9 | static const Mono Character = _$Character; 10 | static const Mono Person = _$Person; 11 | 12 | BangumiContent get bangumiContent { 13 | switch (this) { 14 | case Mono.Character: 15 | return BangumiContent.Character; 16 | case Mono.Person: 17 | return BangumiContent.Person; 18 | default: 19 | assert(false, '$this doesn\'t have a valid bangumiContent type'); 20 | return BangumiContent.CharacterOrPerson; 21 | } 22 | } 23 | 24 | const Mono._(String name) : super(name); 25 | 26 | static BuiltSet get values => _$values; 27 | 28 | static Mono valueOf(String name) => _$valueOf(name); 29 | 30 | static Serializer get serializer => _$monoSerializer; 31 | } 32 | -------------------------------------------------------------------------------- /app/lib/widgets/shared/common/ScaffoldWithRegularAppBar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:munin/styles/theme/Common.dart'; 3 | 4 | /// a simple layout with a scaffold, an AppBat and a inner SafeArea 5 | /// where AppBar is the built-in app bar for [Scaffold] 6 | class ScaffoldWithRegularAppBar extends StatelessWidget { 7 | final Widget safeAreaChild; 8 | final PreferredSizeWidget appBar; 9 | final double safeAreaChildHorizontalPadding; 10 | 11 | const ScaffoldWithRegularAppBar({ 12 | Key key, 13 | @required this.safeAreaChild, 14 | @required this.appBar, 15 | this.safeAreaChildHorizontalPadding = defaultPortraitHorizontalOffset, 16 | }) : super(key: key); 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | return Scaffold( 21 | key: key, 22 | appBar: appBar, 23 | body: SafeArea( 24 | child: Padding( 25 | padding: 26 | EdgeInsets.symmetric(horizontal: safeAreaChildHorizontalPadding), 27 | child: safeAreaChild, 28 | )), 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/lib/widgets/shared/text/MuninTextSpans.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | /// A short cut for [RichText] that displays multiple text spans. 5 | /// 6 | /// This is a simple wrapper around the official widget to reduce boilerplate. 7 | class MuninTextSpans extends StatelessWidget { 8 | final List children; 9 | 10 | const MuninTextSpans({Key key, @required this.children}) : super(key: key); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return RichText( 15 | text: TextSpan( 16 | children: [ 17 | for (var textConfig in children) 18 | TextSpan( 19 | text: textConfig.text, 20 | style: 21 | textConfig.textStyle ?? Theme.of(context).textTheme.bodyText2, 22 | ) 23 | ], 24 | ), 25 | ); 26 | } 27 | } 28 | 29 | class MuninTextSpanConfig { 30 | final String text; 31 | final TextStyle textStyle; 32 | 33 | MuninTextSpanConfig(this.text, [this.textStyle]); 34 | } 35 | -------------------------------------------------------------------------------- /app/lib/models/bangumi/discussion/DiscussionItem.dart: -------------------------------------------------------------------------------- 1 | import 'package:built_value/built_value.dart'; 2 | import 'package:munin/models/bangumi/common/BangumiImage.dart'; 3 | import 'package:munin/models/bangumi/timeline/common/BangumiContent.dart'; 4 | 5 | part 'DiscussionItem.g.dart'; 6 | 7 | /// a base Discussion interface 8 | @BuiltValue(instantiable: false) 9 | abstract class DiscussionItem { 10 | int get id; 11 | 12 | BangumiContent get bangumiContent; 13 | 14 | BangumiImage get image; 15 | 16 | /// For person/character, it's person/character name 17 | /// For episode, it's episode name 18 | /// For group topic/subject topic, it's postName 19 | String get title; 20 | 21 | /// For person/character, it's a fixed string: person/character 22 | /// For episode/subject topic, it's subject name 23 | /// For group topic, it's group name 24 | String get subTitle; 25 | 26 | int get replyCount; 27 | 28 | @nullable 29 | int get updatedAt; 30 | 31 | DiscussionItem rebuild(void updates(DiscussionItemBuilder b)); 32 | 33 | DiscussionItemBuilder toBuilder(); 34 | } 35 | -------------------------------------------------------------------------------- /app/lib/models/bangumi/discussion/thread/common/BangumiThread.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'BangumiThread.dart'; 4 | 5 | // ************************************************************************** 6 | // BuiltValueGenerator 7 | // ************************************************************************** 8 | 9 | abstract class BangumiThreadBuilder { 10 | void replace(BangumiThread other); 11 | void update(void Function(BangumiThreadBuilder) updates); 12 | int get id; 13 | set id(int id); 14 | 15 | String get title; 16 | set title(String title); 17 | 18 | ListBuilder get mainPostReplies; 19 | set mainPostReplies(ListBuilder mainPostReplies); 20 | } 21 | 22 | // ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new 23 | -------------------------------------------------------------------------------- /app/lib/models/bangumi/subject/common/SujectBase.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'SujectBase.dart'; 4 | 5 | // ************************************************************************** 6 | // BuiltValueGenerator 7 | // ************************************************************************** 8 | 9 | abstract class SubjectBaseBuilder { 10 | void replace(SubjectBase other); 11 | void update(void Function(SubjectBaseBuilder) updates); 12 | int get id; 13 | set id(int id); 14 | 15 | String get pageUrlFromApi; 16 | set pageUrlFromApi(String pageUrlFromApi); 17 | 18 | String get name; 19 | set name(String name); 20 | 21 | String get chineseName; 22 | set chineseName(String chineseName); 23 | } 24 | 25 | // ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new 26 | -------------------------------------------------------------------------------- /app/lib/widgets/shared/background/RoundedConcreteBackgroundWithChild.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:munin/styles/theme/Common.dart'; 3 | import 'package:munin/widgets/shared/background/RoundedConcreteBackground.dart'; 4 | import 'package:munin/widgets/shared/text/WrappableText.dart'; 5 | 6 | class RoundedConcreteBackgroundWithChild extends StatelessWidget { 7 | final Widget child; 8 | 9 | const RoundedConcreteBackgroundWithChild({ 10 | @required this.child, 11 | Key key, 12 | }) : super(key: key); 13 | 14 | RoundedConcreteBackgroundWithChild.fromText( 15 | String text, 16 | BuildContext outerContext, { 17 | Key key, 18 | int maxLines, 19 | }) : child = WrappableText( 20 | text, 21 | textStyle: captionTextWithHigherOpacity(outerContext), 22 | outerWrapper: OuterWrapper.Row, 23 | maxLines: maxLines, 24 | ), 25 | super(key: key); 26 | 27 | @override 28 | Widget build(BuildContext context) { 29 | return RoundedConcreteBackground( 30 | child: child, 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/lib/models/bangumi/timeline/common/TimelineFeed.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'TimelineFeed.dart'; 4 | 5 | // ************************************************************************** 6 | // BuiltValueGenerator 7 | // ************************************************************************** 8 | 9 | abstract class TimelineFeedBuilder { 10 | void replace(TimelineFeed other); 11 | void update(void Function(TimelineFeedBuilder) updates); 12 | FeedMetaInfoBuilder get user; 13 | set user(FeedMetaInfoBuilder user); 14 | 15 | BangumiContent get bangumiContent; 16 | set bangumiContent(BangumiContent bangumiContent); 17 | 18 | bool get isFromMutedUser; 19 | set isFromMutedUser(bool isFromMutedUser); 20 | } 21 | 22 | // ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new 23 | -------------------------------------------------------------------------------- /app/lib/models/bangumi/search/result/SearchResultItem.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'SearchResultItem.dart'; 4 | 5 | // ************************************************************************** 6 | // BuiltValueGenerator 7 | // ************************************************************************** 8 | 9 | abstract class SearchResultItemBuilder { 10 | void replace(SearchResultItem other); 11 | void update(void Function(SearchResultItemBuilder) updates); 12 | BangumiImageBuilder get image; 13 | set image(BangumiImageBuilder image); 14 | 15 | String get name; 16 | set name(String name); 17 | 18 | int get id; 19 | set id(int id); 20 | 21 | SearchType get type; 22 | set type(SearchType type); 23 | } 24 | 25 | // ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new 26 | -------------------------------------------------------------------------------- /app/lib/widgets/shared/common/SnackBar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:quiver/time.dart'; 3 | 4 | const Duration shortSnackBarDisplayDuration = aSecond; 5 | const Duration snackBarDisplayDuration = Duration(milliseconds: 4000); 6 | 7 | /// Shows a snackbar on success of [future]. 8 | void showSnackBarOnSuccess( 9 | BuildContext context, 10 | Future future, 11 | String successText, 12 | ) async { 13 | final hasSucceeded = await future; 14 | if (hasSucceeded == true) { 15 | showTextOnSnackBar(context, successText); 16 | } 17 | } 18 | 19 | /// Show text on snackbar, if [shortDuration] is set to true, value in [duration] 20 | /// will be ignored and [shortSnackBarDisplayDuration] will always be used. 21 | void showTextOnSnackBar(BuildContext context, String text, 22 | {duration = snackBarDisplayDuration, shortDuration = false,}) async { 23 | ScaffoldMessenger.of(context).showSnackBar(SnackBar( 24 | content: Text(text), 25 | duration: shortDuration ? shortSnackBarDisplayDuration : duration, 26 | behavior: SnackBarBehavior.floating, 27 | )); 28 | } 29 | -------------------------------------------------------------------------------- /app/lib/widgets/timeline/item/PublicMessageNoReplyWidget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:munin/models/bangumi/timeline/PublicMessageNoReply.dart'; 3 | import 'package:munin/widgets/shared/common/UserActionTile.dart'; 4 | 5 | /// a special public message that's published by system and un-deletable 6 | /// [PublicMessageNameChange] and [PublicMessageSignChange] currently shares this widget 7 | class PublicMessageNoReplyWidget extends StatelessWidget { 8 | final PublicMessageNoReply publicMessageNoReply; 9 | 10 | const PublicMessageNoReplyWidget( 11 | {Key key, @required this.publicMessageNoReply}) 12 | : super(key: key); 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | return Column( 17 | children: [ 18 | UserActionTile.fromUser( 19 | user: publicMessageNoReply.user, 20 | ), 21 | Row( 22 | children: [ 23 | Flexible( 24 | child: Text(publicMessageNoReply.content), 25 | ) 26 | ], 27 | ), 28 | ], 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/lib/models/bangumi/user/notification/BaseNotificationItem.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'BaseNotificationItem.dart'; 4 | 5 | // ************************************************************************** 6 | // BuiltValueGenerator 7 | // ************************************************************************** 8 | 9 | abstract class BaseNotificationItemBuilder { 10 | void replace(BaseNotificationItem other); 11 | void update(void Function(BaseNotificationItemBuilder) updates); 12 | int get id; 13 | set id(int id); 14 | 15 | BangumiUserBasicBuilder get initiator; 16 | set initiator(BangumiUserBasicBuilder initiator); 17 | 18 | String get bodyContentHtml; 19 | set bodyContentHtml(String bodyContentHtml); 20 | } 21 | 22 | // ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new 23 | -------------------------------------------------------------------------------- /app/lib/models/bangumi/timeline/ProgressUpdateEpisodeSingle.dart: -------------------------------------------------------------------------------- 1 | import 'package:built_value/built_value.dart'; 2 | import 'package:built_value/serializer.dart'; 3 | import 'package:munin/models/bangumi/timeline/common/BangumiContent.dart'; 4 | import 'package:munin/models/bangumi/timeline/common/FeedMetaInfo.dart'; 5 | import 'package:munin/models/bangumi/timeline/common/TimelineFeed.dart'; 6 | 7 | part 'ProgressUpdateEpisodeSingle.g.dart'; 8 | 9 | abstract class ProgressUpdateEpisodeSingle 10 | implements 11 | Built, 12 | TimelineFeed { 13 | FeedMetaInfo get user; 14 | 15 | String get episodeName; 16 | 17 | String get episodeId; 18 | 19 | String get subjectName; 20 | 21 | String get subjectId; 22 | 23 | ProgressUpdateEpisodeSingle._(); 24 | 25 | factory ProgressUpdateEpisodeSingle( 26 | [updates(ProgressUpdateEpisodeSingleBuilder b)]) = 27 | _$ProgressUpdateEpisodeSingle; 28 | 29 | static Serializer get serializer => 30 | _$progressUpdateEpisodeSingleSerializer; 31 | } 32 | -------------------------------------------------------------------------------- /app/lib/redux/oauth/OauthActions.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:munin/models/bangumi/BangumiUserSmall.dart'; 3 | 4 | class UpdateLoginDataAction { 5 | final BuildContext context; 6 | final BangumiUserSmall userInfo; 7 | 8 | UpdateLoginDataAction({ 9 | @required this.userInfo, 10 | @required this.context, 11 | }); 12 | } 13 | 14 | class OAuthLoginCancel { 15 | final BuildContext context; 16 | 17 | OAuthLoginCancel(this.context); 18 | } 19 | 20 | class OAuthLoginFailure { 21 | final BuildContext context; 22 | final String message; 23 | 24 | OAuthLoginFailure(this.context, this.message); 25 | } 26 | 27 | class OAuthLoginSuccess { 28 | final BangumiUserSmall userInfo; 29 | 30 | OAuthLoginSuccess(this.userInfo); 31 | } 32 | 33 | class LogoutRequest { 34 | final BuildContext context; 35 | 36 | LogoutRequest(this.context); 37 | } 38 | 39 | class LogoutSuccess { 40 | final BuildContext context; 41 | 42 | LogoutSuccess(this.context); 43 | } 44 | 45 | class LogoutFailure { 46 | final BuildContext context; 47 | 48 | LogoutFailure(this.context); 49 | } 50 | -------------------------------------------------------------------------------- /app/lib/widgets/subject/common/Common.dart: -------------------------------------------------------------------------------- 1 | import 'package:fluro/fluro.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:munin/config/application.dart'; 4 | import 'package:munin/models/bangumi/collection/CollectionStatus.dart'; 5 | import 'package:munin/models/bangumi/subject/BangumiSubject.dart'; 6 | import 'package:munin/router/routes.dart'; 7 | import 'package:munin/shared/utils/misc/constants.dart'; 8 | import 'package:munin/widgets/shared/common/SnackBar.dart'; 9 | 10 | String collectionActionText(BangumiSubject subject) => 11 | subject.userSubjectCollectionInfoPreview.status == CollectionStatus.Pristine 12 | ? '加入收藏' 13 | : '编辑收藏'; 14 | 15 | void navigateToSubjectCollection(BuildContext context, int subjectId) { 16 | showSnackBarOnSuccess( 17 | context, 18 | Application.router.navigateTo( 19 | context, 20 | Routes.subjectCollectionManagementRoute 21 | .replaceFirst(RoutesVariable.subjectIdParam, subjectId?.toString()), 22 | transition: TransitionType.nativeModal, 23 | ), 24 | hasSuccessfullyUpdatedCollectionLabel); 25 | } 26 | -------------------------------------------------------------------------------- /app/lib/widgets/subject/mainpage/CharactersPreview.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:munin/models/bangumi/subject/BangumiSubject.dart'; 3 | import 'package:munin/shared/utils/misc/Launch.dart'; 4 | import 'package:munin/widgets/shared/utils/common.dart'; 5 | import 'package:munin/widgets/subject/common/HorizontalCharacters.dart'; 6 | import 'package:munin/widgets/subject/mainpage/SubjectMoreItemsEntry.dart'; 7 | 8 | class CharactersPreview extends StatelessWidget { 9 | final BangumiSubject subject; 10 | 11 | const CharactersPreview({Key key, @required this.subject}) : super(key: key); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return Column( 16 | children: [ 17 | SubjectMoreItemsEntry( 18 | moreItemsText: '角色介绍', 19 | onTap: () { 20 | launchByPreference(context, 21 | '${httpsBangumiMainSite()}/subject/${subject.id}/characters'); 22 | }, 23 | ), 24 | HorizontalCharacters( 25 | characters: subject.characters, 26 | ) 27 | ], 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/lib/models/bangumi/timeline/FriendshipCreationSingle.dart: -------------------------------------------------------------------------------- 1 | import 'package:built_value/built_value.dart'; 2 | import 'package:built_value/serializer.dart'; 3 | import 'package:munin/models/bangumi/common/BangumiImage.dart'; 4 | import 'package:munin/models/bangumi/timeline/common/BangumiContent.dart'; 5 | import 'package:munin/models/bangumi/timeline/common/FeedMetaInfo.dart'; 6 | import 'package:munin/models/bangumi/timeline/common/TimelineFeed.dart'; 7 | 8 | part 'FriendshipCreationSingle.g.dart'; 9 | 10 | abstract class FriendshipCreationSingle 11 | implements 12 | Built, 13 | TimelineFeed { 14 | FeedMetaInfo get user; 15 | 16 | String get friendNickName; 17 | 18 | BangumiImage get friendAvatar; 19 | 20 | String get friendId; 21 | 22 | FriendshipCreationSingle._(); 23 | 24 | factory FriendshipCreationSingle( 25 | [updates(FriendshipCreationSingleBuilder b)]) = 26 | _$FriendshipCreationSingle; 27 | 28 | static Serializer get serializer => 29 | _$friendshipCreationSingleSerializer; 30 | } 31 | -------------------------------------------------------------------------------- /app/lib/models/bangumi/timeline/PublicMessageNormal.dart: -------------------------------------------------------------------------------- 1 | import 'package:built_value/built_value.dart'; 2 | import 'package:built_value/serializer.dart'; 3 | import 'package:munin/models/bangumi/timeline/common/BangumiContent.dart'; 4 | import 'package:munin/models/bangumi/timeline/common/FeedMetaInfo.dart'; 5 | import 'package:munin/models/bangumi/timeline/common/TimelineFeed.dart'; 6 | 7 | part 'PublicMessageNormal.g.dart'; 8 | 9 | abstract class PublicMessageNormal 10 | implements 11 | Built, 12 | TimelineFeed { 13 | /// due to the limitation of bangumi, this has to be a string 14 | FeedMetaInfo get user; 15 | 16 | /// Content in raw html. 17 | String get contentHtml; 18 | 19 | /// Content in raw text, by parsing html. 20 | String get contentText; 21 | 22 | int get replyCount; 23 | 24 | PublicMessageNormal._(); 25 | 26 | factory PublicMessageNormal([updates(PublicMessageNormalBuilder b)]) = 27 | _$PublicMessageNormal; 28 | 29 | static Serializer get serializer => 30 | _$publicMessageNormalSerializer; 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 edwardez 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 | -------------------------------------------------------------------------------- /app/lib/models/bangumi/setting/theme/ThemeSwitchMode.dart: -------------------------------------------------------------------------------- 1 | import 'package:built_collection/built_collection.dart'; 2 | import 'package:built_value/built_value.dart'; 3 | import 'package:built_value/serializer.dart'; 4 | import 'package:munin/shared/utils/serializers.dart'; 5 | 6 | part 'ThemeSwitchMode.g.dart'; 7 | 8 | class ThemeSwitchMode extends EnumClass { 9 | @BuiltValueEnumConst(fallback: true) 10 | static const ThemeSwitchMode Manual = _$Manual; 11 | 12 | static const ThemeSwitchMode FollowScreenBrightness = 13 | _$FollowScreenBrightness; 14 | 15 | static const ThemeSwitchMode FollowSystemThemeSetting = 16 | _$FollowSystemThemeSetting; 17 | 18 | const ThemeSwitchMode._(String name) : super(name); 19 | 20 | static BuiltSet get values => _$values; 21 | 22 | static ThemeSwitchMode valueOf(String name) => _$valueOf(name); 23 | 24 | static Serializer get serializer => 25 | _$themeSwitchModeSerializer; 26 | 27 | static ThemeSwitchMode fromWiredName(String wiredName) { 28 | return serializers.deserializeWith(ThemeSwitchMode.serializer, wiredName); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /documents/zh-Hans/README.md: -------------------------------------------------------------------------------- 1 | # BangumiN 2 | BangumiN - 使用Angular,Node.js和NoSQL构建的PWA Bangumi客户端。 3 | 4 | ![name](./images/name.png) 5 | 6 | # 注意☠ 7 | 8 | 这个项目仍处于初期开发阶段,功能随时可能在没有通知的情况下发生变化。 9 | 10 | 如果您发现了任何错误,安全漏洞或任何其他异常,欢迎告知! 11 | 12 | 13 | # 安装 14 | 15 | 16 | * 前端 17 | * `npm install` 18 | * `npm run start-dev` 19 | * 后端 20 | * `npm install` 21 | * `npm run start-dev` 22 | 23 | 24 | # 功能 25 | * 作品的搜索,展示,进度管理,状态修改 26 | * 用户/作品评分数据可视化 27 | * 剧透箱:安全的发表剧透 28 | 29 | # 路线图 30 | 31 | | 计划 | 状态/截止日期 | 备注 | 32 | | --- | --- | --- | 33 | | 基本功能:进度管理 | 进行中,2018 Q3 | 基础功能已写完,更新话数进度需要用Virtual Scroll重写 | 34 | | 基础功能:搜索 | 尚未开始, 2018 Q3 | 需要从头重写 | 35 | | 基础功能:作品信息 | 基本完成 | 需要进一步微调css | 36 | | 用户数据可视化 | 2018 Q3 | 进行中 | 37 | | 条目数据可视化 | 2018 Q3 | 尚未开始 | 38 | | 数据管道 | 2018 Q3 | 从Bangumi抓取数据并存到我们的数据库中:用户和条目数据有API,用户的具体条目记录需要使用爬虫 | 39 | | 多主题 | 完成 | 目前有:蓝色,粉色,夜间主题 | 40 | | PWA 优化 | 2018 Q3 | 需要进一步优化 | 41 | | 无障碍支持 | 2018 Q3-Q4 | 尚未开始(aira-label,键盘操控,颜色对比度,语义化标签...) | 42 | | 测试 | 2018 Q4 | 精力不足且网站功能经常变化,等待网站稳定后开始写 | 43 | | 自动化构建,持续集成 | 2018 Q3-Q4 | 网站上线前完成基本构建 | 44 | | 网站部件可拖拽 | 未定 | 等待 https://github.com/angular/material2/issues/8963 | 45 | -------------------------------------------------------------------------------- /app/lib/redux/readme.md: -------------------------------------------------------------------------------- 1 | ### Playbook 2 | 3 | * How to handle redux error and send them back to Widget? 4 | 5 | Unfortunately currently there is no such widely accepted best practice to handle this issue. 6 | So far, the best way seems to be(considering reducing boiler plate / maintainability, but sacrificing some flexibility): 7 | 1. Initialize a `Completer` for evert request action 8 | 2. At the end of each action(i.e. end in the epic, or if necessary, end in the reducer), `complete` 9 | the `Completer`, or `completeError` if an error is thrown. 10 | 3. In the code, now widget gets informed an error is occlude, do what ever you want. 11 | 4. You can use `RequestInProgressIndicatorWidget`, which contains logic to handle most 12 | common error handling cases. It specially useful for `Get` request. 13 | 5. Or you can send the error through `HandleErrorAction`, let relevant epic handle it. 14 | 15 | 16 | * Naming convention 17 | 18 | All reducers, actions, epics..etc should end with `xxxReduer`/`xxxAction`/`xxxEpic` 19 | 20 | * Store 21 | 22 | All variables in store must use `built_value`, otherwise it simply won't work. -------------------------------------------------------------------------------- /app/lib/widgets/shared/text/editor/sticker/BangumiStickers.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:munin/widgets/shared/text/editor/sticker/BangumiSticker.dart'; 3 | 4 | class BangumiStickers extends StatelessWidget { 5 | static const minId = 1; 6 | static const maxId = 123; 7 | 8 | /// A callback function that's called when a specific sticker is tapped. 9 | final Function(int id) onStickerTapped; 10 | 11 | const BangumiStickers({ 12 | Key key, 13 | this.onStickerTapped, 14 | }) : super(key: key); 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | final scaleFactor = MediaQuery.of(context).textScaleFactor; 19 | 20 | return Wrap( 21 | children: [ 22 | for (var i = minId; i < maxId; i++) 23 | InkWell( 24 | child: BangumiSticker( 25 | id: i, 26 | scaleFactor: scaleFactor, 27 | ), 28 | onTap: () { 29 | if (onStickerTapped != null) { 30 | onStickerTapped(i); 31 | } 32 | }, 33 | ) 34 | ], 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/lib/models/bangumi/search/result/BangumiSearchResponse.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'BangumiSearchResponse.dart'; 4 | 5 | // ************************************************************************** 6 | // BuiltValueGenerator 7 | // ************************************************************************** 8 | 9 | abstract class BangumiSearchResponseBuilder { 10 | void replace(BangumiSearchResponse other); 11 | void update(void Function(BangumiSearchResponseBuilder) updates); 12 | int get totalCount; 13 | set totalCount(int totalCount); 14 | 15 | int get requestedResults; 16 | set requestedResults(int requestedResults); 17 | 18 | MapBuilder get results; 19 | set results(MapBuilder results); 20 | } 21 | 22 | // ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new 23 | -------------------------------------------------------------------------------- /app/lib/models/bangumi/discussion/thread/common/GetThreadRequest.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:built_value/built_value.dart'; 4 | import 'package:built_value/serializer.dart'; 5 | import 'package:munin/models/bangumi/discussion/thread/common/ThreadType.dart'; 6 | import 'package:munin/shared/utils/serializers.dart'; 7 | 8 | part 'GetThreadRequest.g.dart'; 9 | 10 | abstract class GetThreadRequest 11 | implements Built { 12 | ThreadType get threadType; 13 | 14 | /// Id of the thread 15 | int get id; 16 | 17 | GetThreadRequest._(); 18 | 19 | factory GetThreadRequest([void Function(GetThreadRequestBuilder) updates]) = 20 | _$GetThreadRequest; 21 | 22 | String toJson() { 23 | return json 24 | .encode(serializers.serializeWith(GetThreadRequest.serializer, this)); 25 | } 26 | 27 | static GetThreadRequest fromJson(String jsonString) { 28 | return serializers.deserializeWith( 29 | GetThreadRequest.serializer, json.decode(jsonString)); 30 | } 31 | 32 | static Serializer get serializer => 33 | _$getThreadRequestSerializer; 34 | } 35 | -------------------------------------------------------------------------------- /app/lib/styles/theme/BrightIonBrownBlue.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:munin/styles/theme/Colors.dart'; 3 | import 'package:munin/styles/theme/Common.dart'; 4 | import 'package:munin/styles/theme/CommonThemeData.dart'; 5 | 6 | final ThemeData brightIonBrownBlueThemeData = ThemeData( 7 | canvasColor: Colors.white, 8 | brightness: Brightness.light, 9 | bottomSheetTheme: muninBottomSheetThemeData(), 10 | buttonTheme: ButtonThemeData( 11 | shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)), 12 | ), 13 | dialogTheme: DialogTheme( 14 | shape: RoundedRectangleBorder(borderRadius: defaultContainerCircularRadius), 15 | ), 16 | appBarTheme: AppBarTheme( 17 | elevation: 0, 18 | brightness: Brightness.light, 19 | color: Colors.white, 20 | iconTheme: IconThemeData(color: Colors.black54), 21 | ), 22 | primaryColor: ionBrown, 23 | // TODO: flutter by default calculates abd set it to [Brightness.dark] 24 | // verify whether [Brightness.light] meets color contrast requirement 25 | primaryColorBrightness: Brightness.light, 26 | accentColor: ionBlue, 27 | toggleableActiveColor: ionBlue, 28 | ); 29 | -------------------------------------------------------------------------------- /app/lib/shared/utils/misc/constants.dart: -------------------------------------------------------------------------------- 1 | import 'package:munin/config/application.dart'; 2 | 3 | const String bangumiAssetsServer = 'lain.bgm.tv'; 4 | final bangumiHostUriForDio = 5 | Uri.parse('https://${Application.environmentValue.bangumiHostForDio}'); 6 | 7 | final String bangumiHomePageUrl = 8 | 'https://$bangumiMainHost'; 9 | final String bangumiTimelineUrl = 10 | 'https://$bangumiMainHost/timeline'; 11 | final String rakuenMobileUrl = 12 | 'https://$bangumiMainHost/m'; 13 | const String bangumiAnonymousUserMediumAvatar = 14 | 'https://$bangumiAssetsServer/pic/user/m/icon.jpg'; 15 | 16 | const String bangumiTextOnlySubjectCover = 17 | 'https://bgm.tv/img/no_icon_subject.png'; 18 | final String bangumiTextOnlyGroupIcon = 19 | 'https://$bangumiAssetsServer/pic/icon/m/no_icon.jpg'; 20 | 21 | const String checkWebVersionLabel = '查看网页版'; 22 | const String goToForsetiLabel = '在bangumin.tv'; 23 | const String appOrBangumiHasAnErrorLabel = '应用或bangumi出错'; 24 | const String openInBrowserLabel = '在浏览器中打开'; 25 | const String hasSuccessfullyUpdatedCollectionLabel = '收藏更新成功'; 26 | 27 | const double avatarSize = 48.0; 28 | const double defaultSliverAppBarElevation = 4.0; 29 | -------------------------------------------------------------------------------- /app/lib/styles/theme/BrightNatoriPinkBrown.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:munin/styles/theme/Colors.dart'; 3 | import 'package:munin/styles/theme/Common.dart'; 4 | import 'package:munin/styles/theme/CommonThemeData.dart'; 5 | 6 | final ThemeData brightNatoriPinkBrownThemeData = ThemeData( 7 | canvasColor: Colors.white, 8 | brightness: Brightness.light, 9 | bottomSheetTheme: muninBottomSheetThemeData(), 10 | buttonTheme: ButtonThemeData( 11 | shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)), 12 | ), 13 | dialogTheme: DialogTheme( 14 | shape: RoundedRectangleBorder(borderRadius: defaultContainerCircularRadius), 15 | ), 16 | appBarTheme: AppBarTheme( 17 | elevation: 0, 18 | brightness: Brightness.light, 19 | color: Colors.white, 20 | iconTheme: IconThemeData(color: Colors.black54), 21 | ), 22 | primaryColor: natoriPink, 23 | // TODO: flutter by default calculates abd set it to [Brightness.dark] 24 | // verify whether [Brightness.light] meets color contrast requirement 25 | primaryColorBrightness: Brightness.light, 26 | accentColor: natoriBrown, 27 | toggleableActiveColor: natoriBrown, 28 | ); 29 | -------------------------------------------------------------------------------- /app/lib/models/bangumi/user/timeline/TimelinePreview.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:built_value/built_value.dart'; 4 | import 'package:built_value/serializer.dart'; 5 | import 'package:munin/shared/utils/serializers.dart'; 6 | 7 | part 'TimelinePreview.g.dart'; 8 | 9 | /// a Timeline that's listed as related Timeline('关联条目') on Timeline main page 10 | abstract class TimelinePreview 11 | implements Built { 12 | /// Content of this timeline in plain text 13 | String get content; 14 | 15 | /// Time when user published this timeline 16 | int get userUpdatedAt; 17 | 18 | TimelinePreview._(); 19 | 20 | factory TimelinePreview([updates(TimelinePreviewBuilder b)]) = 21 | _$TimelinePreview; 22 | 23 | String toJson() { 24 | return json 25 | .encode(serializers.serializeWith(TimelinePreview.serializer, this)); 26 | } 27 | 28 | static TimelinePreview fromJson(String jsonString) { 29 | return serializers.deserializeWith( 30 | TimelinePreview.serializer, json.decode(jsonString)); 31 | } 32 | 33 | static Serializer get serializer => 34 | _$timelinePreviewSerializer; 35 | } 36 | -------------------------------------------------------------------------------- /app/lib/styles/theme/BrightBlueBangumiPink.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:munin/styles/theme/Colors.dart'; 3 | import 'package:munin/styles/theme/Common.dart'; 4 | import 'package:munin/styles/theme/CommonThemeData.dart'; 5 | 6 | final ThemeData brightBangumiPinkBlueThemeData = ThemeData( 7 | canvasColor: Colors.white, 8 | brightness: Brightness.light, 9 | buttonTheme: ButtonThemeData( 10 | shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)), 11 | ), 12 | bottomSheetTheme: muninBottomSheetThemeData(), 13 | dialogTheme: DialogTheme( 14 | shape: RoundedRectangleBorder(borderRadius: defaultContainerCircularRadius), 15 | ), 16 | appBarTheme: AppBarTheme( 17 | elevation: 0, 18 | brightness: Brightness.light, 19 | color: Colors.white, 20 | iconTheme: IconThemeData(color: Colors.black54), 21 | ), 22 | primaryColor: Colors.blue, 23 | // TODO: flutter by default calculates abd set it to [Brightness.dark], need to 24 | // verify whether [Brightness.light] meets color contrast requirement 25 | primaryColorBrightness: Brightness.light, 26 | accentColor: bangumiPink, 27 | toggleableActiveColor: bangumiPink, 28 | ); 29 | -------------------------------------------------------------------------------- /app/lib/models/bangumi/setting/general/browser/BrowserSetting.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:built_value/built_value.dart'; 4 | import 'package:built_value/serializer.dart'; 5 | import 'package:munin/models/bangumi/setting/general/browser/LaunchBrowserPreference.dart'; 6 | import 'package:munin/shared/utils/serializers.dart'; 7 | 8 | part 'BrowserSetting.g.dart'; 9 | 10 | abstract class BrowserSetting 11 | implements Built { 12 | LaunchBrowserPreference get launchBrowserPreference; 13 | 14 | BrowserSetting._(); 15 | 16 | factory BrowserSetting([void Function(BrowserSettingBuilder) updates]) => 17 | _$BrowserSetting((b) => 18 | b.launchBrowserPreference = LaunchBrowserPreference.DefaultInApp); 19 | 20 | String toJson() { 21 | return json 22 | .encode(serializers.serializeWith(BrowserSetting.serializer, this)); 23 | } 24 | 25 | static BrowserSetting fromJson(String jsonString) { 26 | return serializers.deserializeWith( 27 | BrowserSetting.serializer, json.decode(jsonString)); 28 | } 29 | 30 | static Serializer get serializer => 31 | _$browserSettingSerializer; 32 | } 33 | -------------------------------------------------------------------------------- /app/lib/widgets/shared/button/MuninOutlineButton.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:munin/styles/theme/Common.dart'; 3 | 4 | /// A customized outline button which uses primary color for text and outline 5 | /// for bright theme, and accent color for dark theme 6 | class MuninOutlineButton extends StatelessWidget { 7 | /// The button's label. 8 | /// 9 | /// Often a [Text] widget in all caps. 10 | final Widget child; 11 | 12 | /// The callback that is called when the button is tapped or otherwise activated. 13 | /// 14 | /// If this is set to null, the button will be disabled. 15 | final VoidCallback onPressed; 16 | 17 | const MuninOutlineButton( 18 | {Key key, @required this.onPressed, @required this.child}) 19 | : super(key: key); 20 | 21 | @override 22 | Widget build(BuildContext context) { 23 | Color mainColor = lightPrimaryDarkAccentColor(context); 24 | return OutlinedButton( 25 | child: child, 26 | // style: , 27 | // textColor: mainColor, 28 | // borderSide: BorderSide(color: mainColor, width: 1.0), 29 | // highlightedBorderColor: mainColor, 30 | onPressed: onPressed, 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/test/AppCast_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:pub_semver/pub_semver.dart'; 4 | import 'package:test/test.dart'; 5 | import 'package:upgrader/upgrader.dart'; 6 | 7 | void main() { 8 | group('AppCast', () { 9 | String appCastXml; 10 | 11 | setUp(() async { 12 | appCastXml = 13 | File('lib/config/upgrader/production_appcast.xml').readAsStringSync(); 14 | }); 15 | 16 | test('verifies production upgarder xml is valid', () { 17 | final items = Appcast().parseItemsFromXMLString(appCastXml); 18 | 19 | Version newerVersion; 20 | for (var item in items) { 21 | expect(item.fileURL, isNotNull); 22 | expect(item.isCriticalUpdate, isNotNull); 23 | expect(item.itemDescription, isNotNull); 24 | 25 | final currentVersion = Version.parse(item.versionString); 26 | 27 | expect(currentVersion.isPreRelease, isFalse); 28 | if (newerVersion != null) { 29 | // In production, newer version must be higher than older one. 30 | expect(newerVersion.compareTo(currentVersion), isPositive); 31 | } 32 | 33 | newerVersion = currentVersion; 34 | } 35 | }); 36 | }); 37 | } 38 | -------------------------------------------------------------------------------- /app/lib/models/bangumi/timeline/MonoFavoriteSingle.dart: -------------------------------------------------------------------------------- 1 | import 'package:built_value/built_value.dart'; 2 | import 'package:built_value/serializer.dart'; 3 | import 'package:munin/models/bangumi/common/BangumiImage.dart'; 4 | import 'package:munin/models/bangumi/timeline/common/BangumiContent.dart'; 5 | import 'package:munin/models/bangumi/timeline/common/FeedMetaInfo.dart'; 6 | import 'package:munin/models/bangumi/timeline/common/Mono.dart'; 7 | import 'package:munin/models/bangumi/timeline/common/TimelineFeed.dart'; 8 | 9 | part 'MonoFavoriteSingle.g.dart'; 10 | 11 | abstract class MonoFavoriteSingle 12 | implements 13 | Built, 14 | TimelineFeed { 15 | FeedMetaInfo get user; 16 | 17 | String get monoName; 18 | 19 | BangumiImage get avatar; 20 | 21 | /// id on bangumi page, i.e. for https://bgm.tv/character/1 , id is 1 22 | String get id; 23 | 24 | Mono get monoType; 25 | 26 | MonoFavoriteSingle._(); 27 | 28 | factory MonoFavoriteSingle([updates(MonoFavoriteSingleBuilder b)]) = 29 | _$MonoFavoriteSingle; 30 | 31 | static Serializer get serializer => 32 | _$monoFavoriteSingleSerializer; 33 | } 34 | -------------------------------------------------------------------------------- /app/lib/models/bangumi/timeline/common/HyperBangumiItem.dart: -------------------------------------------------------------------------------- 1 | import 'package:built_value/built_value.dart'; 2 | import 'package:built_value/serializer.dart'; 3 | import 'package:munin/models/bangumi/timeline/common/BangumiContent.dart'; 4 | import 'package:munin/models/bangumi/timeline/common/HyperItem.dart'; 5 | 6 | part 'HyperBangumiItem.g.dart'; 7 | 8 | /// A hyper link that directs to a [BangumiContent] page 9 | abstract class HyperBangumiItem 10 | implements Built, HyperItem { 11 | /// id of the hyper text 12 | String get id; 13 | 14 | BangumiContent get contentType; 15 | 16 | /// Item name, i.e. user name, subject name... 17 | String get name; 18 | 19 | /// if we cannot parse content, a fallback webview might be used 20 | /// hence an optional link is needed 21 | @nullable 22 | String get pageUrl; 23 | 24 | /// a item may or may not have an image 25 | @nullable 26 | String get imageUrl; 27 | 28 | HyperBangumiItem._(); 29 | 30 | factory HyperBangumiItem([updates(HyperBangumiItemBuilder b)]) = 31 | _$HyperBangumiItem; 32 | 33 | static Serializer get serializer => 34 | _$hyperBangumiItemSerializer; 35 | } 36 | -------------------------------------------------------------------------------- /app/lib/widgets/timeline/item/common/Actions.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:munin/models/bangumi/timeline/common/GetTimelineRequest.dart'; 3 | import 'package:munin/models/bangumi/timeline/common/TimelineFeed.dart'; 4 | import 'package:munin/redux/app/AppState.dart'; 5 | import 'package:munin/redux/timeline/TimelineActions.dart'; 6 | import 'package:munin/shared/exceptions/utils.dart'; 7 | import 'package:munin/widgets/shared/common/SnackBar.dart'; 8 | import 'package:redux/redux.dart'; 9 | 10 | /// A commonly used helper that deletes a feed. 11 | void deleteFeedHelper( 12 | Store store, 13 | BuildContext context, 14 | GetTimelineRequest getTimelineRequest, 15 | TimelineFeed feed, { 16 | popContextOnSuccess = false, 17 | }) async { 18 | final action = 19 | DeleteTimelineAction(feed: feed, getTimelineRequest: getTimelineRequest); 20 | store.dispatch(action); 21 | 22 | try { 23 | await action.completer.future; 24 | showTextOnSnackBar(context, '时间线删除成功'); 25 | if (popContextOnSuccess) { 26 | Navigator.pop(context); 27 | } 28 | } catch (error) { 29 | showTextOnSnackBar(context, '时间线删除失败: ${formatErrorMessage(error)}'); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/lib/models/bangumi/BangumiCookieCredentials.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:built_value/built_value.dart'; 4 | import 'package:built_value/serializer.dart'; 5 | import 'package:munin/shared/utils/serializers.dart'; 6 | 7 | part 'BangumiCookieCredentials.g.dart'; 8 | 9 | abstract class BangumiCookieCredentials 10 | implements 11 | Built { 12 | BangumiCookieCredentials._(); 13 | 14 | factory BangumiCookieCredentials( 15 | [updates(BangumiCookieCredentialsBuilder b)]) = 16 | _$BangumiCookieCredentials; 17 | 18 | String get authCookie; 19 | 20 | String get sessionCookie; 21 | 22 | String get userAgent; 23 | 24 | DateTime get expiresOn; 25 | 26 | String toJson() { 27 | return json.encode( 28 | serializers.serializeWith(BangumiCookieCredentials.serializer, this)); 29 | } 30 | 31 | static BangumiCookieCredentials fromJson(String jsonString) { 32 | return serializers.deserializeWith( 33 | BangumiCookieCredentials.serializer, json.decode(jsonString)); 34 | } 35 | 36 | static Serializer get serializer => 37 | _$bangumiCookieCredentialsSerializer; 38 | } 39 | -------------------------------------------------------------------------------- /app/lib/redux/search/SearchState.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:built_collection/built_collection.dart'; 4 | import 'package:built_value/built_value.dart'; 5 | import 'package:built_value/serializer.dart'; 6 | import 'package:munin/models/bangumi/search/SearchRequest.dart'; 7 | import 'package:munin/models/bangumi/search/result/BangumiSearchResponse.dart'; 8 | import 'package:munin/shared/utils/serializers.dart'; 9 | 10 | part 'SearchState.g.dart'; 11 | 12 | abstract class SearchState implements Built { 13 | BuiltMap get results; 14 | 15 | factory SearchState([updates(SearchStateBuilder b)]) => _$SearchState((b) => b 16 | ..results.replace(BuiltMap()) 17 | ..update(updates)); 18 | 19 | SearchState._(); 20 | 21 | String toJson() { 22 | return json.encode(serializers.serializeWith(SearchState.serializer, this)); 23 | } 24 | 25 | static SearchState fromJson(String jsonString) { 26 | return serializers.deserializeWith( 27 | SearchState.serializer, json.decode(jsonString)); 28 | } 29 | 30 | static Serializer get serializer => _$searchStateSerializer; 31 | } 32 | -------------------------------------------------------------------------------- /app/lib/styles/theme/NightPureDarkBlue.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:munin/styles/theme/Common.dart'; 3 | import 'package:munin/styles/theme/CommonThemeData.dart'; 4 | 5 | final ThemeData nightPureDarkBlueThemeData = ThemeData( 6 | brightness: Brightness.dark, 7 | canvasColor: Colors.black, 8 | backgroundColor: Colors.black, 9 | scaffoldBackgroundColor: Colors.black, 10 | bottomSheetTheme: muninBottomSheetThemeData(pureDartTheme: true), 11 | buttonTheme: ButtonThemeData( 12 | shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)), 13 | ), 14 | dialogTheme: DialogTheme( 15 | shape: RoundedRectangleBorder(borderRadius: defaultContainerCircularRadius), 16 | ), 17 | appBarTheme: AppBarTheme( 18 | brightness: Brightness.dark, 19 | ), 20 | sliderTheme: SliderThemeData.fromPrimaryColors( 21 | primaryColor: Colors.lightBlueAccent, 22 | primaryColorDark: Colors.lightBlueAccent.shade700, 23 | valueIndicatorTextStyle: ThemeData().textTheme.bodyText1, 24 | primaryColorLight: Colors.lightBlueAccent.shade100, 25 | ), 26 | dividerColor: Colors.grey, 27 | accentColor: Colors.lightBlueAccent, 28 | toggleableActiveColor: Colors.lightBlueAccent, 29 | ); 30 | -------------------------------------------------------------------------------- /app/lib/widgets/discussion/thread/shared/CopyPostContent.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:html/parser.dart'; 3 | import 'package:munin/widgets/shared/icons/AdaptiveIcons.dart'; 4 | import 'package:munin/widgets/shared/services/Clipboard.dart'; 5 | 6 | class CopyPostContent extends StatelessWidget { 7 | final String contentHtml; 8 | 9 | /// An additional context that's under a scaffold. If non-null, copy snackbar 10 | /// messages are shown under [contextWithScaffold]. 11 | /// It's useful if [CopyPostContent] itself is put in a place with no scaffold. 12 | final BuildContext contextWithScaffold; 13 | 14 | const CopyPostContent( 15 | {Key key, @required this.contentHtml, this.contextWithScaffold}) 16 | : super(key: key); 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | return ListTile( 21 | leading: Icon(AdaptiveIcons.clipBoardIconData), 22 | title: Text('复制内容'), 23 | onTap: () { 24 | Navigator.pop(context); 25 | var postContent = (parseFragment(contentHtml).text ?? '').trim(); 26 | ClipboardService.copyAsPlainText( 27 | contextWithScaffold ?? context, postContent); 28 | }, 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/lib/redux/progress/common.dart: -------------------------------------------------------------------------------- 1 | import 'package:munin/models/bangumi/progress/api/EpisodeProgress.dart'; 2 | import 'package:munin/models/bangumi/progress/common/BaseEpisode.dart'; 3 | import 'package:munin/models/bangumi/progress/common/EpisodeStatus.dart'; 4 | import 'package:munin/models/bangumi/progress/common/EpisodeType.dart'; 5 | import 'package:munin/models/bangumi/progress/common/EpisodeUpdateType.dart'; 6 | 7 | /// Checks if episode will be affected by [EpisodeUpdateType.CollectUntil] 8 | bool isEpisodeAffectedByCollectUntilOperation(BaseEpisode episode) { 9 | return episode.episodeType == EpisodeType.Regular && 10 | episode.userEpisodeStatus != EpisodeStatus.Dropped; 11 | } 12 | 13 | /// Watch until only affects previous episodes with smaller sequential number that 14 | /// are regular episodes and status is not [EpisodeStatus.Dropped]. 15 | /// [EpisodeProgress] has an additional [sequentialNumber] which can be used to 16 | /// check which episodes are behind new EpisodeNumber. 17 | bool isEpisodeProgressAffectedByCollectUntilOperation( 18 | EpisodeProgress episode, int newEpisodeNumber) { 19 | return isEpisodeAffectedByCollectUntilOperation(episode) && 20 | episode.sequentialNumber <= newEpisodeNumber; 21 | } 22 | -------------------------------------------------------------------------------- /app/lib/models/bangumi/progress/common/BaseEpisode.dart: -------------------------------------------------------------------------------- 1 | import 'package:built_value/built_value.dart'; 2 | import 'package:munin/models/bangumi/progress/common/AirStatus.dart'; 3 | import 'package:munin/models/bangumi/progress/common/EpisodeStatus.dart'; 4 | import 'package:munin/models/bangumi/progress/common/EpisodeType.dart'; 5 | 6 | part 'BaseEpisode.g.dart'; 7 | 8 | @BuiltValue(instantiable: false) 9 | abstract class BaseEpisode { 10 | /// id of this episode. This id can be used to uniquely identify an episode 11 | /// across all bangumi subjects 12 | int get id; 13 | 14 | String get name; 15 | 16 | @BuiltValueField(wireName: 'name_cn') 17 | String get chineseName; 18 | 19 | /// Current air status of this episode(has been aired or not) 20 | @BuiltValueField(wireName: 'status') 21 | AirStatus get airStatus; 22 | 23 | /// Current user episode status of this episode(has been watched or not) 24 | /// Note: user can mark an episode as 'watched' regardless its airStatus 25 | @BuiltValueField(wireName: 'user_episode_status') 26 | EpisodeStatus get userEpisodeStatus; 27 | 28 | EpisodeType get episodeType; 29 | 30 | BaseEpisode rebuild(void updates(BaseEpisodeBuilder b)); 31 | 32 | BaseEpisodeBuilder toBuilder(); 33 | } 34 | -------------------------------------------------------------------------------- /app/lib/models/bangumi/discussion/GroupDiscussionItem.dart: -------------------------------------------------------------------------------- 1 | import 'package:built_value/built_value.dart'; 2 | import 'package:built_value/serializer.dart'; 3 | import 'package:munin/models/bangumi/common/BangumiImage.dart'; 4 | import 'package:munin/models/bangumi/discussion/DiscussionItem.dart'; 5 | import 'package:munin/models/bangumi/timeline/common/BangumiContent.dart'; 6 | 7 | part 'GroupDiscussionItem.g.dart'; 8 | 9 | abstract class GroupDiscussionItem 10 | implements 11 | Built, 12 | DiscussionItem { 13 | @nullable 14 | String get originalPosterUsername; 15 | 16 | /// Secondary user identifier, for Rakuen posts, `originalPosterUsername` is 17 | /// not available, instead, there is `originalPosterUserId` 18 | /// User id is parsed from path in user icon 19 | /// meaning for user with default icon, this will be null 20 | @nullable 21 | int get originalPosterUserId; 22 | 23 | String get postedGroupId; 24 | 25 | GroupDiscussionItem._(); 26 | 27 | factory GroupDiscussionItem([updates(GroupDiscussionItemBuilder b)]) = 28 | _$GroupDiscussionItem; 29 | 30 | static Serializer get serializer => 31 | _$groupDiscussionItemSerializer; 32 | } 33 | -------------------------------------------------------------------------------- /app/lib/models/bangumi/discussion/thread/common/OriginalPost.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:built_value/built_value.dart'; 4 | import 'package:built_value/serializer.dart'; 5 | import 'package:munin/models/bangumi/common/BangumiUserBasic.dart'; 6 | import 'package:munin/models/bangumi/discussion/thread/common/Post.dart'; 7 | import 'package:munin/shared/utils/serializers.dart'; 8 | 9 | part 'OriginalPost.g.dart'; 10 | 11 | /// OriginalPost post in a thread that's posted by the original author. 12 | abstract class OriginalPost 13 | implements Post, Built { 14 | @override 15 | String get sequentialName { 16 | return '#$mainSequentialNumber'; 17 | } 18 | 19 | OriginalPost._(); 20 | 21 | factory OriginalPost([void Function(OriginalPostBuilder) updates]) = 22 | _$OriginalPost; 23 | 24 | String toJson() { 25 | return json 26 | .encode(serializers.serializeWith(OriginalPost.serializer, this)); 27 | } 28 | 29 | static OriginalPost fromJson(String jsonString) { 30 | return serializers.deserializeWith( 31 | OriginalPost.serializer, json.decode(jsonString)); 32 | } 33 | 34 | static Serializer get serializer => _$originalPostSerializer; 35 | } 36 | -------------------------------------------------------------------------------- /app/lib/widgets/subject/management/StarRatingFormField.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:munin/styles/theme/Common.dart'; 3 | import 'package:munin/widgets/subject/management/StarRating.dart'; 4 | 5 | class StarRatingFormField extends FormField { 6 | StarRatingFormField( 7 | {FormFieldSetter onSaved, 8 | FormFieldValidator validator, 9 | int initialValue = 0, 10 | double horizontalPadding = defaultPortraitHorizontalOffset, 11 | AutovalidateMode autovalidate = AutovalidateMode.disabled}) 12 | : super( 13 | onSaved: onSaved, 14 | validator: validator, 15 | initialValue: initialValue, 16 | autovalidateMode: autovalidate, 17 | builder: (FormFieldState state) { 18 | return StarRating( 19 | rating: state.value.toDouble(), 20 | onRatingChanged: (double rating) => 21 | state.didChange(rating.toInt()), 22 | horizontalPadding: horizontalPadding); 23 | }); 24 | 25 | @override 26 | _StarRatingFormFieldState createState() => _StarRatingFormFieldState(); 27 | } 28 | 29 | class _StarRatingFormFieldState extends FormFieldState {} 30 | -------------------------------------------------------------------------------- /app/lib/models/bangumi/timeline/CollectionUpdateSingle.dart: -------------------------------------------------------------------------------- 1 | import 'package:built_value/built_value.dart'; 2 | import 'package:built_value/serializer.dart'; 3 | import 'package:munin/models/bangumi/common/BangumiImage.dart'; 4 | import 'package:munin/models/bangumi/timeline/common/BangumiContent.dart'; 5 | import 'package:munin/models/bangumi/timeline/common/FeedMetaInfo.dart'; 6 | import 'package:munin/models/bangumi/timeline/common/TimelineFeed.dart'; 7 | 8 | part 'CollectionUpdateSingle.g.dart'; 9 | 10 | abstract class CollectionUpdateSingle 11 | implements 12 | Built, 13 | TimelineFeed { 14 | /// due to the limitation of bangumi, this has to be a string 15 | FeedMetaInfo get user; 16 | 17 | @nullable 18 | String get subjectComment; 19 | 20 | String get subjectId; 21 | 22 | BangumiImage get subjectCover; 23 | 24 | @nullable 25 | double get subjectScore; 26 | 27 | String get subjectName; 28 | 29 | CollectionUpdateSingle._(); 30 | 31 | factory CollectionUpdateSingle([updates(CollectionUpdateSingleBuilder b)]) = 32 | _$CollectionUpdateSingle; 33 | 34 | static Serializer get serializer => 35 | _$collectionUpdateSingleSerializer; 36 | } 37 | -------------------------------------------------------------------------------- /app/lib/models/bangumi/discussion/GeneralDiscussionItem.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:built_value/built_value.dart'; 4 | import 'package:built_value/serializer.dart'; 5 | import 'package:munin/models/bangumi/common/BangumiImage.dart'; 6 | import 'package:munin/models/bangumi/discussion/DiscussionItem.dart'; 7 | import 'package:munin/models/bangumi/timeline/common/BangumiContent.dart'; 8 | import 'package:munin/shared/utils/serializers.dart'; 9 | 10 | part 'GeneralDiscussionItem.g.dart'; 11 | 12 | abstract class GeneralDiscussionItem 13 | implements 14 | Built, 15 | DiscussionItem { 16 | factory GeneralDiscussionItem([updates(GeneralDiscussionItemBuilder b)]) = 17 | _$GeneralDiscussionItem; 18 | 19 | GeneralDiscussionItem._(); 20 | 21 | String toJson() { 22 | return json.encode( 23 | serializers.serializeWith(GeneralDiscussionItem.serializer, this)); 24 | } 25 | 26 | static GeneralDiscussionItem fromJson(String jsonString) { 27 | return serializers.deserializeWith( 28 | GeneralDiscussionItem.serializer, json.decode(jsonString)); 29 | } 30 | 31 | static Serializer get serializer => 32 | _$generalDiscussionItemSerializer; 33 | } 34 | -------------------------------------------------------------------------------- /app/lib/models/bangumi/user/social/NetworkServiceTagLink.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:built_value/built_value.dart'; 4 | import 'package:built_value/serializer.dart'; 5 | import 'package:munin/models/bangumi/user/social/NetworkServiceTag.dart'; 6 | import 'package:munin/models/bangumi/user/social/NetworkServiceType.dart'; 7 | import 'package:munin/shared/utils/serializers.dart'; 8 | 9 | part 'NetworkServiceTagLink.g.dart'; 10 | 11 | abstract class NetworkServiceTagLink 12 | implements 13 | NetworkServiceTag, 14 | Built { 15 | NetworkServiceTagLink._(); 16 | 17 | String get link; 18 | 19 | factory NetworkServiceTagLink( 20 | [void Function(NetworkServiceTagLinkBuilder) updates]) = 21 | _$NetworkServiceTagLink; 22 | 23 | String toJson() { 24 | return json.encode( 25 | serializers.serializeWith(NetworkServiceTagLink.serializer, this)); 26 | } 27 | 28 | static NetworkServiceTagLink fromJson(String jsonString) { 29 | return serializers.deserializeWith( 30 | NetworkServiceTagLink.serializer, json.decode(jsonString)); 31 | } 32 | 33 | static Serializer get serializer => 34 | _$networkServiceTagLinkSerializer; 35 | } 36 | -------------------------------------------------------------------------------- /app/lib/widgets/subject/mainpage/RelatedSubjectsPreview.dart: -------------------------------------------------------------------------------- 1 | import 'package:built_collection/built_collection.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:munin/models/bangumi/setting/general/PreferredSubjectInfoLanguage.dart'; 4 | import 'package:munin/models/bangumi/subject/RelatedSubject.dart'; 5 | import 'package:munin/widgets/subject/common/HorizontalRelatedSubjects.dart'; 6 | import 'package:munin/widgets/subject/mainpage/SubjectMoreItemsEntry.dart'; 7 | 8 | class RelatedSubjectsPreview extends StatelessWidget { 9 | final BuiltListMultimap relatedSubjects; 10 | 11 | final PreferredSubjectInfoLanguage preferredSubjectInfoLanguage; 12 | 13 | const RelatedSubjectsPreview({Key key, 14 | @required this.relatedSubjects, 15 | @required this.preferredSubjectInfoLanguage}) 16 | : super(key: key); 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | return Column( 21 | children: [ 22 | SubjectMoreItemsEntry( 23 | moreItemsText: '关联条目', 24 | ), 25 | HorizontalRelatedSubjects( 26 | relatedSubjects: relatedSubjects.values, 27 | preferredSubjectInfoLanguage: preferredSubjectInfoLanguage, 28 | ), 29 | ], 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/lib/models/bangumi/progress/common/BaseEpisode.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'BaseEpisode.dart'; 4 | 5 | // ************************************************************************** 6 | // BuiltValueGenerator 7 | // ************************************************************************** 8 | 9 | abstract class BaseEpisodeBuilder { 10 | void replace(BaseEpisode other); 11 | void update(void Function(BaseEpisodeBuilder) updates); 12 | int get id; 13 | set id(int id); 14 | 15 | String get name; 16 | set name(String name); 17 | 18 | String get chineseName; 19 | set chineseName(String chineseName); 20 | 21 | AirStatus get airStatus; 22 | set airStatus(AirStatus airStatus); 23 | 24 | EpisodeStatus get userEpisodeStatus; 25 | set userEpisodeStatus(EpisodeStatus userEpisodeStatus); 26 | 27 | EpisodeType get episodeType; 28 | set episodeType(EpisodeType episodeType); 29 | } 30 | 31 | // ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new 32 | -------------------------------------------------------------------------------- /app/test/shared/utils/misc/DeviceInfo_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:munin/shared/utils/misc/DeviceInfo.dart'; 2 | import 'package:pub_semver/pub_semver.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('parseIOSVersion', () { 7 | test('Parses 13', () { 8 | expect(VersionExtension.parseIOSVersion('13'), Version(13, 0, 0)); 9 | }); 10 | 11 | test('Parses 13.0', () { 12 | expect(VersionExtension.parseIOSVersion('13.0'), Version(13, 0, 0)); 13 | }); 14 | 15 | test('Parses 13.1', () { 16 | expect(VersionExtension.parseIOSVersion('13.1'), Version(13, 1, 0)); 17 | }); 18 | 19 | test('Parses 13.1.2', () { 20 | expect(VersionExtension.parseIOSVersion('13.1.2'), Version(13, 1, 2)); 21 | }); 22 | 23 | test('Throws error on 13.1.2.1', () { 24 | expect(VersionExtension.parseIOSVersion('13.1.2'), Version(13, 1, 2)); 25 | }); 26 | 27 | test('Parses 13.1.2.1 and ignores version string after last minor', () { 28 | expect(VersionExtension.parseIOSVersion('13.1.2.1'), Version(13, 1, 2)); 29 | }); 30 | 31 | test('Throws error on invalidVersion', () { 32 | expect(() => VersionExtension.parseIOSVersion('invalidVersion'), 33 | throwsA(TypeMatcher())); 34 | }); 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /app/lib/widgets/shared/cover/ClickableCachedRoundedCover.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:munin/models/bangumi/timeline/common/BangumiContent.dart'; 3 | import 'package:munin/widgets/shared/cover/CachedRoundedCover.dart'; 4 | import 'package:munin/widgets/shared/utils/common.dart'; 5 | 6 | class ClickableCachedRoundedCover extends StatelessWidget { 7 | final String imageUrl; 8 | final String id; 9 | final double width; 10 | final double height; 11 | final BangumiContent contentType; 12 | 13 | ClickableCachedRoundedCover( 14 | {@required this.imageUrl, 15 | @required this.contentType, 16 | @required this.width, 17 | @required this.height, 18 | @required this.id}); 19 | 20 | ClickableCachedRoundedCover.asGridSize({ 21 | @required this.imageUrl, 22 | @required this.contentType, 23 | @required this.id, 24 | this.width = 48.0, 25 | this.height = 48.0, 26 | }); 27 | 28 | @override 29 | Widget build(BuildContext context) { 30 | return InkWell( 31 | child: CachedRoundedCover( 32 | imageUrl: this.imageUrl, width: this.width, height: this.height), 33 | onTap: generateOnTapCallbackForBangumiContent( 34 | contentType: contentType, id: id, context: context), 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/lib/models/bangumi/timeline/message/PublicMessageReply.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:built_value/built_value.dart'; 4 | import 'package:built_value/serializer.dart'; 5 | import 'package:munin/models/bangumi/common/BangumiUserBasic.dart'; 6 | import 'package:munin/shared/utils/serializers.dart'; 7 | 8 | part 'PublicMessageReply.g.dart'; 9 | 10 | abstract class PublicMessageReply 11 | implements Built { 12 | /// author of the reply. 13 | BangumiUserBasic get author; 14 | 15 | /// Reply content in html. 16 | String get contentHtml; 17 | 18 | /// Reply content in plainText. 19 | String get contentText; 20 | 21 | PublicMessageReply._(); 22 | 23 | factory PublicMessageReply( 24 | [void Function(PublicMessageReplyBuilder) updates]) = 25 | _$PublicMessageReply; 26 | 27 | String toJson() { 28 | return json 29 | .encode(serializers.serializeWith(PublicMessageReply.serializer, this)); 30 | } 31 | 32 | static PublicMessageReply fromJson(String jsonString) { 33 | return serializers.deserializeWith( 34 | PublicMessageReply.serializer, json.decode(jsonString)); 35 | } 36 | 37 | static Serializer get serializer => 38 | _$publicMessageReplySerializer; 39 | } 40 | -------------------------------------------------------------------------------- /app/lib/models/bangumi/subject/common/ParentSubject.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:built_value/built_value.dart'; 4 | import 'package:built_value/serializer.dart'; 5 | import 'package:munin/models/bangumi/common/BangumiImage.dart'; 6 | import 'package:munin/models/bangumi/subject/common/SujectBase.dart'; 7 | import 'package:munin/shared/utils/serializers.dart'; 8 | 9 | part 'ParentSubject.g.dart'; 10 | 11 | /// A subject as seen on subject page. 12 | /// It's called a 'parent' subject because it's typically shown on subject episode, 13 | /// subject thread pages where these items are linked to a parent subject. 14 | abstract class ParentSubject 15 | implements SubjectBase, Built { 16 | BangumiImage get cover; 17 | 18 | ParentSubject._(); 19 | 20 | factory ParentSubject([void Function(ParentSubjectBuilder) updates]) = 21 | _$ParentSubject; 22 | 23 | String toJson() { 24 | return json 25 | .encode(serializers.serializeWith(ParentSubject.serializer, this)); 26 | } 27 | 28 | static ParentSubject fromJson(String jsonString) { 29 | return serializers.deserializeWith( 30 | ParentSubject.serializer, json.decode(jsonString)); 31 | } 32 | 33 | static Serializer get serializer => _$parentSubjectSerializer; 34 | } 35 | -------------------------------------------------------------------------------- /app/lib/models/bangumi/discussion/thread/common/Post.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'Post.dart'; 4 | 5 | // ************************************************************************** 6 | // BuiltValueGenerator 7 | // ************************************************************************** 8 | 9 | abstract class PostBuilder { 10 | void replace(Post other); 11 | void update(void Function(PostBuilder) updates); 12 | BangumiUserBasicBuilder get author; 13 | set author(BangumiUserBasicBuilder author); 14 | 15 | int get id; 16 | set id(int id); 17 | 18 | String get contentHtml; 19 | set contentHtml(String contentHtml); 20 | 21 | String get authorPostedText; 22 | set authorPostedText(String authorPostedText); 23 | 24 | int get postTimeInMilliSeconds; 25 | set postTimeInMilliSeconds(int postTimeInMilliSeconds); 26 | 27 | int get mainSequentialNumber; 28 | set mainSequentialNumber(int mainSequentialNumber); 29 | } 30 | 31 | // ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new 32 | -------------------------------------------------------------------------------- /app/lib/models/bangumi/user/social/NetworkServiceTagPlainText.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:built_value/built_value.dart'; 4 | import 'package:built_value/serializer.dart'; 5 | import 'package:munin/models/bangumi/user/social/NetworkServiceTag.dart'; 6 | import 'package:munin/models/bangumi/user/social/NetworkServiceType.dart'; 7 | import 'package:munin/shared/utils/serializers.dart'; 8 | 9 | part 'NetworkServiceTagPlainText.g.dart'; 10 | 11 | abstract class NetworkServiceTagPlainText 12 | implements 13 | NetworkServiceTag, 14 | Built { 15 | NetworkServiceTagPlainText._(); 16 | 17 | factory NetworkServiceTagPlainText( 18 | [void Function(NetworkServiceTagPlainTextBuilder) updates]) = 19 | _$NetworkServiceTagPlainText; 20 | 21 | String toJson() { 22 | return json.encode( 23 | serializers.serializeWith(NetworkServiceTagPlainText.serializer, this)); 24 | } 25 | 26 | static NetworkServiceTagPlainText fromJson(String jsonString) { 27 | return serializers.deserializeWith( 28 | NetworkServiceTagPlainText.serializer, json.decode(jsonString)); 29 | } 30 | 31 | static Serializer get serializer => 32 | _$networkServiceTagPlainTextSerializer; 33 | } 34 | -------------------------------------------------------------------------------- /app/lib/widgets/shared/chips/StrokeChip.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | /// A [ChoiceChip] that has a background which is by default same as 4 | /// CanvasColor of the Theme, textColor/borderColor is the same, as set by the 5 | /// pass-in parameter. 6 | /// 7 | class StrokeChip extends StatelessWidget { 8 | /// Callback when the chip is pressed. 9 | /// If [onPressed] is null, a [RawChip] instead of [ActionChip] will be used. 10 | final VoidCallback onPressed; 11 | 12 | final Color borderColor; 13 | 14 | /// Label on the chip. 15 | final Widget label; 16 | 17 | const StrokeChip({ 18 | Key key, 19 | @required this.borderColor, 20 | @required this.label, 21 | this.onPressed, 22 | }) : super(key: key); 23 | 24 | @override 25 | Widget build(BuildContext context) { 26 | final shape = StadiumBorder( 27 | side: BorderSide( 28 | color: borderColor, 29 | )); 30 | 31 | if (onPressed == null) { 32 | return RawChip( 33 | label: label, 34 | backgroundColor: Theme.of(context).canvasColor, 35 | shape: shape, 36 | ); 37 | } 38 | 39 | return ActionChip( 40 | onPressed: onPressed, 41 | label: label, 42 | backgroundColor: Theme.of(context).canvasColor, 43 | shape: shape, 44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/lib/models/bangumi/user/notification/GeneralNotificationItem.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:built_value/built_value.dart'; 4 | import 'package:built_value/serializer.dart'; 5 | import 'package:munin/models/bangumi/common/BangumiUserBasic.dart'; 6 | import 'package:munin/models/bangumi/user/notification/BaseNotificationItem.dart'; 7 | import 'package:munin/shared/utils/serializers.dart'; 8 | 9 | part 'GeneralNotificationItem.g.dart'; 10 | 11 | /// A general-purpose notification item. 12 | abstract class GeneralNotificationItem 13 | implements 14 | Built, 15 | BaseNotificationItem { 16 | GeneralNotificationItem._(); 17 | 18 | factory GeneralNotificationItem( 19 | [void Function(GeneralNotificationItemBuilder) updates]) = 20 | _$GeneralNotificationItem; 21 | 22 | String toJson() { 23 | return json.encode( 24 | serializers.serializeWith(GeneralNotificationItem.serializer, this)); 25 | } 26 | 27 | static GeneralNotificationItem fromJson(String jsonString) { 28 | return serializers.deserializeWith( 29 | GeneralNotificationItem.serializer, json.decode(jsonString)); 30 | } 31 | 32 | static Serializer get serializer => 33 | _$generalNotificationItemSerializer; 34 | } 35 | -------------------------------------------------------------------------------- /app/lib/widgets/setting/general/BrowserSettingWidget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:munin/models/bangumi/setting/general/browser/LaunchBrowserPreference.dart'; 3 | import 'package:munin/widgets/shared/selection/MuninExpansionSelection.dart'; 4 | 5 | class BrowserSettingWidget extends StatelessWidget { 6 | final LaunchBrowserPreference currentLaunchBrowserPreference; 7 | 8 | final Function(LaunchBrowserPreference launchBrowserPreference) onTapOption; 9 | 10 | const BrowserSettingWidget({ 11 | Key key, 12 | @required this.currentLaunchBrowserPreference, 13 | @required this.onTapOption, 14 | }) : super(key: key); 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return MuninExpansionSelection( 19 | expansionKey: 20 | PageStorageKey('general-setting-LaunchBrowserPreference'), 21 | title: Text('打开链接时使用'), 22 | optionTitleBuilder: (selection) => Text(selection.chineseName), 23 | optionSubTitleBuilder: (selection) { 24 | return Text(selection.subChineseName); 25 | }, 26 | options: LaunchBrowserPreference.values, 27 | currentSelection: currentLaunchBrowserPreference ?? 28 | LaunchBrowserPreference.DefaultInApp, 29 | onTapOption: onTapOption, 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/ios/Runner/GoogleService-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CLIENT_ID 6 | 674801028596-rae532fpdlshhba5at0bkudfh0d2dbj6.apps.googleusercontent.com 7 | REVERSED_CLIENT_ID 8 | com.googleusercontent.apps.674801028596-rae532fpdlshhba5at0bkudfh0d2dbj6 9 | API_KEY 10 | AIzaSyCT23Jxa4PcW7JO8jkh9eWeRg8wcIUkbAQ 11 | GCM_SENDER_ID 12 | 674801028596 13 | PLIST_VERSION 14 | 1 15 | BUNDLE_ID 16 | com.bangumin.munin.ios 17 | PROJECT_ID 18 | bangumin-app 19 | STORAGE_BUCKET 20 | bangumin-app.appspot.com 21 | IS_ADS_ENABLED 22 | 23 | IS_ANALYTICS_ENABLED 24 | 25 | IS_APPINVITE_ENABLED 26 | 27 | IS_GCM_ENABLED 28 | 29 | IS_SIGNIN_ENABLED 30 | 31 | GOOGLE_APP_ID 32 | 1:674801028596:ios:3e039bc7540951cb 33 | DATABASE_URL 34 | https://bangumin-app.firebaseio.com 35 | 36 | -------------------------------------------------------------------------------- /app/lib/models/bangumi/discussion/DiscussionItem.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'DiscussionItem.dart'; 4 | 5 | // ************************************************************************** 6 | // BuiltValueGenerator 7 | // ************************************************************************** 8 | 9 | abstract class DiscussionItemBuilder { 10 | void replace(DiscussionItem other); 11 | void update(void Function(DiscussionItemBuilder) updates); 12 | int get id; 13 | set id(int id); 14 | 15 | BangumiContent get bangumiContent; 16 | set bangumiContent(BangumiContent bangumiContent); 17 | 18 | BangumiImageBuilder get image; 19 | set image(BangumiImageBuilder image); 20 | 21 | String get title; 22 | set title(String title); 23 | 24 | String get subTitle; 25 | set subTitle(String subTitle); 26 | 27 | int get replyCount; 28 | set replyCount(int replyCount); 29 | 30 | int get updatedAt; 31 | set updatedAt(int updatedAt); 32 | } 33 | 34 | // ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new 35 | -------------------------------------------------------------------------------- /app/lib/redux/search/SearchReducer.dart: -------------------------------------------------------------------------------- 1 | import 'package:munin/models/bangumi/search/SearchRequest.dart'; 2 | import 'package:munin/models/bangumi/search/result/BangumiGeneralSearchResponse.dart'; 3 | import 'package:munin/redux/search/SearchActions.dart'; 4 | import 'package:munin/redux/search/SearchState.dart'; 5 | import 'package:redux/redux.dart'; 6 | 7 | final searchReducers = combineReducers([ 8 | /// Search Subject 9 | TypedReducer(searchSubjectSuccessReducer), 10 | ]); 11 | 12 | SearchState searchSubjectSuccessReducer( 13 | SearchState searchState, SearchSuccessAction searchSubjectSuccessAction) { 14 | SearchRequest searchRequest = searchSubjectSuccessAction.searchRequest; 15 | BangumiGeneralSearchResponse responseToUpdate = 16 | searchState.results[searchRequest]; 17 | BangumiGeneralSearchResponse newResponse = 18 | searchSubjectSuccessAction.searchResponse; 19 | if (responseToUpdate == null) { 20 | responseToUpdate = newResponse; 21 | } else { 22 | responseToUpdate = responseToUpdate.rebuild((b) => b 23 | ..requestedResults = b.requestedResults + newResponse.requestedResults 24 | ..results.addAll(newResponse.results.asMap())); 25 | } 26 | 27 | return searchState.rebuild( 28 | (b) => b..results.addAll({searchRequest: responseToUpdate}), 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BangumiN 2 | 3 | 4 | 5 | 6 | BangumiN - Yet another Bangumi app. 7 | 8 | BangumiN is a native client written in Flutter, an app that aims to provide essential Bangumi features, 9 | munin runs on modern iOS/Android/(+?) systems. 10 | 11 | Disclaimer: this is a third-party app, we are not affiliated with the Bangumi officials. 12 | 13 |     14 | 15 | # Preview 16 | 17 | home page 18 | 19 | 20 | # Install 21 | 22 | * For app 23 | 24 | See instructions in app folder 25 | -------------------------------------------------------------------------------- /app/lib/models/bangumi/BangumiUserIdentity.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:built_value/built_value.dart'; 4 | import 'package:built_value/serializer.dart'; 5 | import 'package:munin/shared/utils/serializers.dart'; 6 | 7 | part 'BangumiUserIdentity.g.dart'; 8 | 9 | abstract class BangumiUserIdentity 10 | implements Built { 11 | BangumiUserIdentity._(); 12 | 13 | factory BangumiUserIdentity([updates(BangumiUserIdentityBuilder b)]) = 14 | _$BangumiUserIdentity; 15 | 16 | @BuiltValueField(wireName: 'access_token') 17 | String get accessToken; 18 | 19 | @BuiltValueField(wireName: 'client_id') 20 | String get clientId; 21 | 22 | @BuiltValueField(wireName: 'expires') 23 | int get expires; 24 | 25 | @BuiltValueField(wireName: 'scope') 26 | @nullable 27 | String get scope; 28 | 29 | @BuiltValueField(wireName: 'user_id') 30 | int get id; 31 | 32 | String toJson() { 33 | return json.encode( 34 | serializers.serializeWith(BangumiUserIdentity.serializer, this)); 35 | } 36 | 37 | static BangumiUserIdentity fromJson(String jsonString) { 38 | return serializers.deserializeWith( 39 | BangumiUserIdentity.serializer, json.decode(jsonString)); 40 | } 41 | 42 | static Serializer get serializer => 43 | _$bangumiUserIdentitySerializer; 44 | } 45 | -------------------------------------------------------------------------------- /app/lib/providers/storage/SecureStorageService.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_secure_storage/flutter_secure_storage.dart'; 2 | import 'package:meta/meta.dart'; 3 | import 'package:munin/models/bangumi/BangumiCookieCredentials.dart'; 4 | import 'package:oauth2/oauth2.dart'; 5 | 6 | class SecureStorageService { 7 | final FlutterSecureStorage secureStorage; 8 | 9 | static const String cookieCredentialsKey = 'bangumiCookieCredentials'; 10 | static const String oauthCredentialsKey = 'bangumiOauthCredentials'; 11 | 12 | SecureStorageService({@required this.secureStorage}); 13 | 14 | Future persistBangumiOauthCredentials(Credentials credentials) async { 15 | assert(credentials != null); 16 | return this 17 | .secureStorage 18 | .write(key: oauthCredentialsKey, value: credentials.toJson()); 19 | } 20 | 21 | Future clearBangumiOauthCredentials() async { 22 | return this.secureStorage.delete(key: oauthCredentialsKey); 23 | } 24 | 25 | Future persistBangumiCookieCredentials( 26 | BangumiCookieCredentials credentials) async { 27 | assert(credentials != null); 28 | return this 29 | .secureStorage 30 | .write(key: cookieCredentialsKey, value: credentials.toJson()); 31 | } 32 | 33 | Future clearBangumiCookieCredentials() async { 34 | return this.secureStorage.delete(key: cookieCredentialsKey); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/lib/models/bangumi/progress/html/SubjectEpisodes.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:built_collection/built_collection.dart'; 4 | import 'package:built_value/built_value.dart'; 5 | import 'package:built_value/serializer.dart'; 6 | import 'package:munin/models/bangumi/progress/html/SimpleHtmlBasedEpisode.dart'; 7 | import 'package:munin/models/bangumi/subject/common/ParentSubject.dart'; 8 | import 'package:munin/shared/utils/serializers.dart'; 9 | 10 | part 'SubjectEpisodes.g.dart'; 11 | 12 | /// All episodes of a subject, as seen on bangumi web page, like https://bgm.tv/subject/1836/ep. 13 | abstract class SubjectEpisodes 14 | implements Built { 15 | @nullable 16 | ParentSubject get subject; 17 | 18 | BuiltMap get episodes; 19 | 20 | SubjectEpisodes._(); 21 | 22 | factory SubjectEpisodes([void Function(SubjectEpisodesBuilder) updates]) = 23 | _$SubjectEpisodes; 24 | 25 | String toJson() { 26 | return json 27 | .encode(serializers.serializeWith(SubjectEpisodes.serializer, this)); 28 | } 29 | 30 | static SubjectEpisodes fromJson(String jsonString) { 31 | return serializers.deserializeWith( 32 | SubjectEpisodes.serializer, json.decode(jsonString)); 33 | } 34 | 35 | static Serializer get serializer => 36 | _$subjectEpisodesSerializer; 37 | } 38 | -------------------------------------------------------------------------------- /app/lib/models/bangumi/search/result/MonoSearchResult.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:built_collection/built_collection.dart'; 4 | import 'package:built_value/built_value.dart'; 5 | import 'package:built_value/serializer.dart'; 6 | import 'package:munin/models/bangumi/common/BangumiImage.dart'; 7 | import 'package:munin/models/bangumi/search/SearchType.dart'; 8 | import 'package:munin/models/bangumi/search/result/SearchResultItem.dart'; 9 | import 'package:munin/shared/utils/serializers.dart'; 10 | 11 | part 'MonoSearchResult.g.dart'; 12 | 13 | abstract class MonoSearchResult 14 | implements 15 | SearchResultItem, 16 | Built { 17 | BuiltList get miscInfo; 18 | 19 | @BuiltValueField(wireName: 'name_cn') 20 | String get chineseName; 21 | 22 | factory MonoSearchResult([updates(MonoSearchResultBuilder b)]) = 23 | _$MonoSearchResult; 24 | 25 | MonoSearchResult._(); 26 | 27 | String toJson() { 28 | return json 29 | .encode(serializers.serializeWith(MonoSearchResult.serializer, this)); 30 | } 31 | 32 | static MonoSearchResult fromJson(String jsonString) { 33 | return serializers.deserializeWith( 34 | MonoSearchResult.serializer, json.decode(jsonString)); 35 | } 36 | 37 | static Serializer get serializer => 38 | _$monoSearchResultSerializer; 39 | } 40 | -------------------------------------------------------------------------------- /app/lib/models/bangumi/subject/common/SubjectBaseWithCover.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:built_value/built_value.dart'; 4 | import 'package:built_value/serializer.dart'; 5 | import 'package:munin/models/bangumi/common/BangumiImage.dart'; 6 | import 'package:munin/models/bangumi/subject/common/SujectBase.dart'; 7 | import 'package:munin/shared/utils/serializers.dart'; 8 | 9 | part 'SubjectBaseWithCover.g.dart'; 10 | 11 | /// a subject that has a cover. 12 | abstract class SubjectBaseWithCover 13 | implements 14 | SubjectBase, 15 | Built { 16 | /// Images might be intentionally set to null because 17 | /// of [displayedAsPlainText] in [CollectionPreview] 18 | @nullable 19 | BangumiImage get cover; 20 | 21 | SubjectBaseWithCover._(); 22 | 23 | factory SubjectBaseWithCover([updates(SubjectBaseWithCoverBuilder b)]) = 24 | _$SubjectBaseWithCover; 25 | 26 | String toJson() { 27 | return json.encode( 28 | serializers.serializeWith(SubjectBaseWithCover.serializer, this)); 29 | } 30 | 31 | static SubjectBaseWithCover fromJson(String jsonString) { 32 | return serializers.deserializeWith( 33 | SubjectBaseWithCover.serializer, json.decode(jsonString)); 34 | } 35 | 36 | static Serializer get serializer => 37 | _$subjectBaseWithCoverSerializer; 38 | } 39 | -------------------------------------------------------------------------------- /app/lib/models/bangumi/subject/RelatedSubject.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:built_value/built_value.dart'; 4 | import 'package:built_value/serializer.dart'; 5 | import 'package:munin/models/bangumi/common/BangumiImage.dart'; 6 | import 'package:munin/models/bangumi/subject/common/SujectBase.dart'; 7 | import 'package:munin/shared/utils/serializers.dart'; 8 | 9 | part 'RelatedSubject.g.dart'; 10 | 11 | /// a subject that's listed as related subject('关联条目') on subject main page 12 | abstract class RelatedSubject 13 | implements SubjectBase, Built { 14 | /// i.e. 画集, 原声集, 片头曲... 15 | /// ideally this should be a enum, however due to the nature of parsing and 16 | /// the number of subtype, it's currently a string 17 | String get subjectSubTypeName; 18 | 19 | @nullable 20 | BangumiImage get cover; 21 | 22 | RelatedSubject._(); 23 | 24 | factory RelatedSubject([updates(RelatedSubjectBuilder b)]) = _$RelatedSubject; 25 | 26 | String toJson() { 27 | return json 28 | .encode(serializers.serializeWith(RelatedSubject.serializer, this)); 29 | } 30 | 31 | static RelatedSubject fromJson(String jsonString) { 32 | return serializers.deserializeWith( 33 | RelatedSubject.serializer, json.decode(jsonString)); 34 | } 35 | 36 | static Serializer get serializer => 37 | _$relatedSubjectSerializer; 38 | } 39 | -------------------------------------------------------------------------------- /app/lib/models/bangumi/subject/info/InfoBoxItem.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:built_value/built_value.dart'; 4 | import 'package:built_value/serializer.dart'; 5 | import 'package:munin/models/bangumi/timeline/common/BangumiContent.dart'; 6 | import 'package:munin/shared/utils/serializers.dart'; 7 | 8 | part 'InfoBoxItem.g.dart'; 9 | 10 | /// The smallest element in a info box 11 | /// i.e. a staff under a job 12 | abstract class InfoBoxItem implements Built { 13 | InfoBoxItem._(); 14 | 15 | BangumiContent get type; 16 | 17 | /// Item name, i.e. staff name, air date... 18 | String get name; 19 | 20 | @nullable 21 | 22 | /// id of the InfoBoxItem, if there is any. For plain text such id doesn't exist 23 | String get id; 24 | 25 | /// if we cannot parse content, a fallback webview might be used 26 | /// hence an optional link is needed 27 | @nullable 28 | String get pageUrl; 29 | 30 | factory InfoBoxItem([updates(InfoBoxItemBuilder b)]) = _$InfoBoxItem; 31 | 32 | String toJson() { 33 | return json.encode(serializers.serializeWith(InfoBoxItem.serializer, this)); 34 | } 35 | 36 | static InfoBoxItem fromJson(String jsonString) { 37 | return serializers.deserializeWith( 38 | InfoBoxItem.serializer, json.decode(jsonString)); 39 | } 40 | 41 | static Serializer get serializer => _$infoBoxItemSerializer; 42 | } 43 | -------------------------------------------------------------------------------- /app/lib/redux/shared/RequestStatus.dart: -------------------------------------------------------------------------------- 1 | import 'package:built_collection/built_collection.dart'; 2 | import 'package:built_value/built_value.dart'; 3 | import 'package:built_value/serializer.dart'; 4 | 5 | part 'RequestStatus.g.dart'; 6 | 7 | class RequestStatus extends EnumClass { 8 | /// Request has not started yet 9 | static const RequestStatus Initial = _$Initial; 10 | 11 | static const RequestStatus Loading = _$Loading; 12 | static const RequestStatus Success = _$Success; 13 | 14 | /// New exception/error should be added to [isException] 15 | static const RequestStatus TimeoutException = _$Timeout; 16 | static const RequestStatus UnknownException = _$UnknownError; 17 | 18 | /// If new status is a exception/error, please add it here 19 | @memoized 20 | bool get isException { 21 | return this == RequestStatus.TimeoutException || 22 | this == RequestStatus.UnknownException; 23 | } 24 | 25 | /// Checks whether next request can be initialized. 26 | @memoized 27 | bool get canInitializeNextRequest { 28 | return this == RequestStatus.Initial || this == RequestStatus.Success; 29 | } 30 | 31 | const RequestStatus._(String name) : super(name); 32 | 33 | static BuiltSet get values => _$values; 34 | 35 | static RequestStatus valueOf(String name) => _$valueOf(name); 36 | 37 | static Serializer get serializer => _$requestStatusSerializer; 38 | } 39 | --------------------------------------------------------------------------------