feat: global subtitle display setting

You can now activate/deactivate the visiblity of
subtitles on a global application level.
This commit is contained in:
4gray committed 2021-09-05 16:39:56 +02:00
1 parent 3dd2789a36
commit 4d2e17565d
15 files changed
+209 -144

No files matched your search

@@ -26,6 +26,9 @@ export class HtmlVideoPlayerComponent implements OnChanges, OnDestroy {
/** HLS object */ /** HLS object */
hls: Hls; hls: Hls;
/** Captions/subtitles indicator */
@Input() showCaptions!: boolean;
/** /**
* Listen for component input changes * Listen for component input changes
* @param changes component changes * @param changes component changes
@@ -54,6 +57,19 @@ export class HtmlVideoPlayerComponent implements OnChanges, OnDestroy {
} }
} }
/**
* Disables text based captions based on the global settings
*/
disableCaptions(): void {
for (
let i = 0;
i < this.videoPlayer.nativeElement.textTracks.length;
i++
) {
this.videoPlayer.nativeElement.textTracks[i].mode = 'hidden';
}
}
/** /**
* Handles promise based play operation * Handles promise based play operation
*/ */
@@ -64,6 +80,9 @@ export class HtmlVideoPlayerComponent implements OnChanges, OnDestroy {
playPromise playPromise
.then((_) => { .then((_) => {
// Automatic playback started! // Automatic playback started!
if (!this.showCaptions) {
this.disableCaptions();
}
}) })
.catch((error) => {}); .catch((error) => {});
} }
@@ -51,7 +51,7 @@
<!-- video.js player --> <!-- video.js player -->
<app-vjs-player <app-vjs-player
*ngIf="player === 'videojs'" *ngIf="playerSettings.player === 'videojs'"
[options]="{ [options]="{
sources: [ sources: [
{ {
@@ -60,12 +60,14 @@
} }
] ]
}" }"
[class.hide-captions]="!playerSettings.showCaptions"
></app-vjs-player> ></app-vjs-player>
</ng-container> </ng-container>
<!-- default html player component --> <!-- default html player component -->
<app-html-video-player <app-html-video-player
*ngIf="player === 'html5'" *ngIf="playerSettings.player === 'html5'"
[channel]="activeChannel$ | async" [channel]="activeChannel$ | async"
[showCaptions]="playerSettings.showCaptions"
> >
</app-html-video-player> </app-html-video-player>
@@ -16,6 +16,7 @@ import * as MOCKED_PLAYLIST from '../../../../mocks/playlist.json';
import { createChannel } from '../../../state'; import { createChannel } from '../../../state';
import { HtmlVideoPlayerComponent } from '../html-video-player/html-video-player.component'; import { HtmlVideoPlayerComponent } from '../html-video-player/html-video-player.component';
import { EpgListComponent } from '../epg-list/epg-list.component'; import { EpgListComponent } from '../epg-list/epg-list.component';
import { VideoPlayer } from '../../../settings/settings.interface';
class MatSnackBarStub { class MatSnackBarStub {
open(): void {} open(): void {}
@@ -73,7 +74,10 @@ describe('VideoPlayerComponent', () => {
it('should check default component settings', () => { it('should check default component settings', () => {
fixture.detectChanges(); fixture.detectChanges();
expect(component.player).toEqual('videojs'); expect(component.playerSettings).toEqual({
player: VideoPlayer.VideoJs,
showCaptions: false,
});
}); });
it('should update store after channel was faved', () => { it('should update store after channel was faved', () => {
@@ -5,10 +5,7 @@ import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators'; import { tap } from 'rxjs/operators';
import { MatSidenav } from '@angular/material/sidenav'; import { MatSidenav } from '@angular/material/sidenav';
import { StorageMap } from '@ngx-pwa/local-storage'; import { StorageMap } from '@ngx-pwa/local-storage';
import { import { Settings, VideoPlayer } from '../../../settings/settings.interface';
Settings,
VideoPlayerType,
} from '../../../settings/settings.interface';
import { MatSnackBar } from '@angular/material/snack-bar'; import { MatSnackBar } from '@angular/material/snack-bar';
/** Settings key in storage */ /** Settings key in storage */
@@ -46,8 +43,11 @@ export class VideoPlayerComponent implements OnInit {
(store) => store.favorites (store) => store.favorites
); );
/** Selected video player component */ /** Selected video player options */
player: VideoPlayerType = 'videojs'; playerSettings: Partial<Settings> = {
player: VideoPlayer.VideoJs,
showCaptions: false,
};
/** Sidebar object */ /** Sidebar object */
@ViewChild('sidenav') sideNav: MatSidenav; @ViewChild('sidenav') sideNav: MatSidenav;
@@ -79,7 +79,10 @@ export class VideoPlayerComponent implements OnInit {
applySettings(): void { applySettings(): void {
this.storage.get(SETTINGS_STORE_KEY).subscribe((settings: Settings) => { this.storage.get(SETTINGS_STORE_KEY).subscribe((settings: Settings) => {
if (settings && Object.keys(settings).length > 0) { if (settings && Object.keys(settings).length > 0) {
this.player = settings.player; this.playerSettings = {
player: settings.player,
showCaptions: settings.showCaptions,
};
} }
}); });
} }
@@ -5,3 +5,12 @@
width: 100% !important; width: 100% !important;
height: calc(100vh - 85px); height: calc(100vh - 85px);
} }
/* Hide the captions settings item from the captions menu. */
.hide-captions {
.vjs-texttrack-settings,
.vjs-text-track-display,
.vjs-subs-caps-button {
display: none;
}
}
+10
View File
@@ -119,6 +119,16 @@
</p> </p>
</div> </div>
</mat-list-item> </mat-list-item>
<mat-list-item>
<div matLine fxLayout="row">
<p fxFlex>{{ 'SETTINGS.SHOW_CAPTIONS' | translate }}</p>
<p fxFlex>
<mat-checkbox
formControlName="showCaptions"
></mat-checkbox>
</p>
</div>
</mat-list-item>
<mat-list-item> <mat-list-item>
<div matLine fxLayout="row"> <div matLine fxLayout="row">
<p fxFlex>{{ 'SETTINGS.VERSION' | translate }}</p> <p fxFlex>{{ 'SETTINGS.VERSION' | translate }}</p>
@@ -1,3 +1,4 @@
import { MatCheckboxModule } from '@angular/material/checkbox';
import { StorageMap } from '@ngx-pwa/local-storage'; import { StorageMap } from '@ngx-pwa/local-storage';
import { FormBuilder } from '@angular/forms'; import { FormBuilder } from '@angular/forms';
/* eslint-disable @typescript-eslint/unbound-method */ /* eslint-disable @typescript-eslint/unbound-method */
@@ -72,6 +73,7 @@ describe('SettingsComponent', () => {
MockModule(MatCardModule), MockModule(MatCardModule),
MockModule(MatListModule), MockModule(MatListModule),
MockModule(MatFormFieldModule), MockModule(MatFormFieldModule),
MockModule(MatCheckboxModule),
], ],
}).compileComponents(); }).compileComponents();
}) })
+11 -5
View File
@@ -5,7 +5,7 @@ import { StorageMap } from '@ngx-pwa/local-storage';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { MatSnackBar } from '@angular/material/snack-bar'; import { MatSnackBar } from '@angular/material/snack-bar';
import { Observable, Subscription } from 'rxjs'; import { Observable, Subscription } from 'rxjs';
import { Settings } from './settings.interface'; import { Settings, VideoPlayer } from './settings.interface';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import * as semver from 'semver'; import * as semver from 'semver';
import { ElectronService } from '../services/electron.service'; import { ElectronService } from '../services/electron.service';
@@ -34,11 +34,11 @@ export class SettingsComponent implements OnInit, OnDestroy {
/** Player options */ /** Player options */
players = [ players = [
{ {
id: 'html5', id: VideoPlayer.Html5Player,
label: 'HTML5 Video Player', label: 'HTML5 Video Player',
}, },
{ {
id: 'videojs', id: VideoPlayer.VideoJs,
label: 'VideoJs Player', label: 'VideoJs Player',
}, },
]; ];
@@ -84,9 +84,10 @@ export class SettingsComponent implements OnInit, OnDestroy {
private translate: TranslateService private translate: TranslateService
) { ) {
this.settingsForm = this.formBuilder.group({ this.settingsForm = this.formBuilder.group({
player: ['videojs'], player: [VideoPlayer.VideoJs],
epgUrl: '', epgUrl: '',
language: Language.ENGLISH, language: Language.ENGLISH,
showCaptions: false,
theme: Theme.LightTheme, theme: Theme.LightTheme,
}); });
@@ -123,11 +124,16 @@ export class SettingsComponent implements OnInit, OnDestroy {
this.storage.get('settings').subscribe((settings: Settings) => { this.storage.get('settings').subscribe((settings: Settings) => {
if (settings) { if (settings) {
this.settingsForm.setValue({ this.settingsForm.setValue({
player: settings.player ? settings.player : 'videojs', player: settings.player
? settings.player
: VideoPlayer.VideoJs,
epgUrl: settings.epgUrl ? settings.epgUrl : '', epgUrl: settings.epgUrl ? settings.epgUrl : '',
language: settings.language language: settings.language
? settings.language ? settings.language
: Language.ENGLISH, : Language.ENGLISH,
showCaptions: settings.showCaptions
? settings.showCaptions
: false,
theme: settings.theme theme: settings.theme
? settings.theme ? settings.theme
: Theme.LightTheme, : Theme.LightTheme,
+6 -2
View File
@@ -1,14 +1,18 @@
import { Language } from './language.enum'; import { Language } from './language.enum';
import { Theme } from './theme.enum'; import { Theme } from './theme.enum';
export type VideoPlayerType = 'html5' | 'videojs'; export enum VideoPlayer {
VideoJs = 'videojs',
Html5Player = 'html5',
}
/** /**
* Describes all available settings options of the application * Describes all available settings options of the application
*/ */
export interface Settings { export interface Settings {
player: VideoPlayerType; player: VideoPlayer;
epgUrl: string; epgUrl: string;
language: Language; language: Language;
showCaptions: boolean;
theme: Theme; theme: Theme;
} }
+3 -2
View File
@@ -24,7 +24,7 @@
"PROGRAM_DIALOG": { "PROGRAM_DIALOG": {
"PROGRAM_DETAILS": "Details zum TV-Programm", "PROGRAM_DETAILS": "Details zum TV-Programm",
"TITLE": "Titel", "TITLE": "Titel",
"CATEGORY": "Ketegorie", "CATEGORY": "Kategorie",
"PARENTAL_RATING_SYSTEM": "Alterskennzeichnung (USK)", "PARENTAL_RATING_SYSTEM": "Alterskennzeichnung (USK)",
"DESCRIPTION": "Beschreibung", "DESCRIPTION": "Beschreibung",
"LANGUAGE": "Sprache", "LANGUAGE": "Sprache",
@@ -105,7 +105,8 @@
"VIDEO_PLAYER_LABEL": "Video player", "VIDEO_PLAYER_LABEL": "Video player",
"VIDEO_PLAYER_PLACEHOLDER": "Option auswählen", "VIDEO_PLAYER_PLACEHOLDER": "Option auswählen",
"SETTINGS_SAVED": "Einstellungen wurden erfolgreich gespeichert!", "SETTINGS_SAVED": "Einstellungen wurden erfolgreich gespeichert!",
"THEME": "Farbdesign" "THEME": "Farbdesign",
"SHOW_CAPTIONS": "Untertitel anzeigen"
}, },
"THEMES": { "THEMES": {
"DARK_THEME": "Dunkel", "DARK_THEME": "Dunkel",
+2 -1
View File
@@ -65,7 +65,8 @@
"LATEST_VERSION": "You are using the latest version", "LATEST_VERSION": "You are using the latest version",
"LANGUAGE": "Language", "LANGUAGE": "Language",
"SETTINGS_SAVED": "Success! Configuration was saved.", "SETTINGS_SAVED": "Success! Configuration was saved.",
"THEME": "Visual theme" "THEME": "Visual theme",
"SHOW_CAPTIONS": "Show subtitles"
}, },
"THEMES": { "THEMES": {
"DARK_THEME": "Dark theme", "DARK_THEME": "Dark theme",
+2 -1
View File
@@ -65,7 +65,8 @@
"LATEST_VERSION": "Estás usando la versión reciente", "LATEST_VERSION": "Estás usando la versión reciente",
"LANGUAGE": "Idioma", "LANGUAGE": "Idioma",
"SETTINGS_SAVED": "¡Completado! La configuración se guardó.", "SETTINGS_SAVED": "¡Completado! La configuración se guardó.",
"THEME": "Tema visual" "THEME": "Tema visual",
"SHOW_CAPTIONS": "Mostrar subtítulos"
}, },
"THEMES": { "THEMES": {
"DARK_THEME": "Tema oscuro", "DARK_THEME": "Tema oscuro",
+2 -1
View File
@@ -65,7 +65,8 @@
"LATEST_VERSION": "최신 버전을 사용하고 있습니다", "LATEST_VERSION": "최신 버전을 사용하고 있습니다",
"LANGUAGE": "언어", "LANGUAGE": "언어",
"SETTINGS_SAVED": "설정이 저장되었습니다.", "SETTINGS_SAVED": "설정이 저장되었습니다.",
"THEME": "테마" "THEME": "테마",
"SHOW_CAPTIONS": "Show subtitles"
}, },
"THEMES": { "THEMES": {
"DARK_THEME": "어두운 테마", "DARK_THEME": "어두운 테마",
+2 -1
View File
@@ -65,7 +65,8 @@
"LATEST_VERSION": "Вы используете последнюю версию приложения", "LATEST_VERSION": "Вы используете последнюю версию приложения",
"LANGUAGE": "Язык", "LANGUAGE": "Язык",
"SETTINGS_SAVED": "Новые настройки были успешно сохранены!", "SETTINGS_SAVED": "Новые настройки были успешно сохранены!",
"THEME": "Тема оформления" "THEME": "Тема оформления",
"SHOW_CAPTIONS": "Отображать субтитры"
}, },
"THEMES": { "THEMES": {
"DARK_THEME": "Тёмная тема", "DARK_THEME": "Тёмная тема",
+122 -121
View File
@@ -1,121 +1,122 @@
{ {
"HOME": { "HOME": {
"TITLE": "IPTVnator", "TITLE": "IPTVnator",
"SUBTITLE": "选择一个播放列表或者从URL/文件中添加一个新的播放列表", "SUBTITLE": "选择一个播放列表或者从URL/文件中添加一个新的播放列表",
"TABS": { "TABS": {
"RECENTLY_ADDED": "最近添加的播放列表", "RECENTLY_ADDED": "最近添加的播放列表",
"FILE_UPLOAD": "从文件添加", "FILE_UPLOAD": "从文件添加",
"URL_UPLOAD": "从URL链接添加" "URL_UPLOAD": "从URL链接添加"
}, },
"PLAYLISTS": { "PLAYLISTS": {
"NO_PLAYLISTS": "未添加播放列表", "NO_PLAYLISTS": "未添加播放列表",
"ADD_FIRST": "从其他标签页添加你的第一个播放列表", "ADD_FIRST": "从其他标签页添加你的第一个播放列表",
"ADDED_VIA_URL": "添加自URL链接:", "ADDED_VIA_URL": "添加自URL链接:",
"ADDED_VIA_FILE": "添加自文件", "ADDED_VIA_FILE": "添加自文件",
"CHANNELS": "频道", "CHANNELS": "频道",
"ADDED": "已添加的", "ADDED": "已添加的",
"SHOW_DETAILS": "显示播放列表详情", "SHOW_DETAILS": "显示播放列表详情",
"REMOVE": "移除播放列表", "REMOVE": "移除播放列表",
"INFO_DIALOG": { "INFO_DIALOG": {
"PLAYLIST_DETAILS": "播放列表详情", "PLAYLIST_DETAILS": "播放列表详情",
"TITLE": "标题", "TITLE": "标题",
"FROM_URL": "添加自URL链接", "FROM_URL": "添加自URL链接",
"IMPORT_DATE": "导入日期", "IMPORT_DATE": "导入日期",
"CHANNELS": "频道数", "CHANNELS": "频道数",
"CLOSE": "关闭", "CLOSE": "关闭",
"SAVE": "保存", "SAVE": "保存",
"USER_AGENT": "User agent", "USER_AGENT": "User agent",
"ORIGINAL_FILENAME": "原始文件名", "ORIGINAL_FILENAME": "原始文件名",
"FILE_PATH": "文件路径", "FILE_PATH": "文件路径",
"UPDATE_FAILED": "上次播放列表更新失败", "UPDATE_FAILED": "上次播放列表更新失败",
"AUTO_UPDATE": "自动更新", "AUTO_UPDATE": "自动更新",
"AUTO_UPDATE_DESCRIPTION": "如果启用了自动刷新功能,则每次启动应用程序时都会自动更新播放列表。", "AUTO_UPDATE_DESCRIPTION": "如果启用了自动刷新功能,则每次启动应用程序时都会自动更新播放列表。",
"CUSTOM_USER_AGENT": "一些 IPTV 提供商需要特定的用户代理才能播放他们的播放列表。" "CUSTOM_USER_AGENT": "一些 IPTV 提供商需要特定的用户代理才能播放他们的播放列表。"
}, },
"REMOVE_DIALOG": { "REMOVE_DIALOG": {
"TITLE": "移除播放列表", "TITLE": "移除播放列表",
"MESSAGE": "您确定要完全删除此播放列表吗?" "MESSAGE": "您确定要完全删除此播放列表吗?"
}, },
"UPDATED": "更新", "UPDATED": "更新",
"REFRESH": "刷新播放列表" "REFRESH": "刷新播放列表"
}, },
"FILE_UPLOAD": { "FILE_UPLOAD": {
"DRAG_DROP": "将文件拖放到此处", "DRAG_DROP": "将文件拖放到此处",
"OR": "或", "OR": "或",
"CHOOSE_PLAYLIST": "选择一个播放列表" "CHOOSE_PLAYLIST": "选择一个播放列表"
}, },
"URL_UPLOAD": { "URL_UPLOAD": {
"PLAYLIST_URL": "播放一个播放列表(m3u, m3u8)", "PLAYLIST_URL": "播放一个播放列表(m3u, m3u8)",
"ADD_PLAYLIST": "添加播放列表" "ADD_PLAYLIST": "添加播放列表"
} }
}, },
"SETTINGS": { "SETTINGS": {
"TITLE": "设置 / IPTVnator", "TITLE": "设置 / IPTVnator",
"SUBTITLE": "更改应用程序的配置 ", "SUBTITLE": "更改应用程序的配置 ",
"DESCRIPTION": "请调整以下列表中的选项并使用保存按钮确认", "DESCRIPTION": "请调整以下列表中的选项并使用保存按钮确认",
"EPG_URL_LABEL": "电子节目单EPG Url (*.xml 或 *.xml.gz)", "EPG_URL_LABEL": "电子节目单EPG Url (*.xml 或 *.xml.gz)",
"EPG_URL_PLACEHOLDER": "输入一个 URL", "EPG_URL_PLACEHOLDER": "输入一个 URL",
"REFRESH_EPG": "刷新电子节目单EPG", "REFRESH_EPG": "刷新电子节目单EPG",
"VIDEO_PLAYER_LABEL": "视频播放器", "VIDEO_PLAYER_LABEL": "视频播放器",
"VIDEO_PLAYER_PLACEHOLDER": "选择一个选项", "VIDEO_PLAYER_PLACEHOLDER": "选择一个选项",
"VERSION": "版本", "VERSION": "版本",
"SAVE_CHANGES": "保存更改", "SAVE_CHANGES": "保存更改",
"BACK_TO_HOME": "回到主页", "BACK_TO_HOME": "回到主页",
"NEW_VERSION_AVAILABLE": "有新版本可用 ", "NEW_VERSION_AVAILABLE": "有新版本可用 ",
"LATEST_VERSION": "您使用的是最新版本", "LATEST_VERSION": "您使用的是最新版本",
"LANGUAGE": "语言", "LANGUAGE": "语言",
"SETTINGS_SAVED": "配置已成功保存", "SETTINGS_SAVED": "配置已成功保存",
"THEME": "主题" "THEME": "主题",
}, "SHOW_CAPTIONS": "Show subtitles"
"THEMES": { },
"DARK_THEME": "暗黑模式", "THEMES": {
"LIGHT_THEME": "明亮模式" "DARK_THEME": "暗黑模式",
}, "LIGHT_THEME": "明亮模式"
"CHANNELS": { },
"SEARCH_CHANNEL": "搜索频道", "CHANNELS": {
"UPLOAD_OR_SELECT_OTHER_PLAYLIST": "上传或选择其他播放列表", "SEARCH_CHANNEL": "搜索频道",
"ALL_CHANNELS": "所有频道", "UPLOAD_OR_SELECT_OTHER_PLAYLIST": "上传或选择其他播放列表",
"GROUPS": "分组", "ALL_CHANNELS": "所有频道",
"UNGROUPED": "未分组", "GROUPS": "分组",
"UNNAMED_CHANNEL": "未命名频道", "UNGROUPED": "未分组",
"FAVORITES": "收藏", "UNNAMED_CHANNEL": "未命名频道",
"NO_FAVORITES": "还没有收藏夹", "FAVORITES": "收藏",
"USE_STAR_TO_FAVORITE": "使用“☆”图标将频道添加到此列表中", "NO_FAVORITES": "还没有收藏夹",
"REMOVE_FAVORITE": "从收藏夹中删除" "USE_STAR_TO_FAVORITE": "使用“☆”图标将频道添加到此列表中",
}, "REMOVE_FAVORITE": "从收藏夹中删除"
"TOP_MENU": { },
"OPEN_CHANNELS_LIST": "打开频道列表", "TOP_MENU": {
"TOGGLE_FAVORITE_FLAG": "切换收藏夹标志", "OPEN_CHANNELS_LIST": "打开频道列表",
"OPEN_EPG_LIST": "打开电子节目单EPG列表" "TOGGLE_FAVORITE_FLAG": "切换收藏夹标志",
}, "OPEN_EPG_LIST": "打开电子节目单EPG列表"
"EPG": { },
"NEXT_DAY": "明天", "EPG": {
"PREVIOUS_DAY": "昨天", "NEXT_DAY": "明天",
"LIVE_NOW": "Live now", "PREVIOUS_DAY": "昨天",
"LIVE_STREAM": "Live stream", "LIVE_NOW": "Live now",
"TIMESHIFT_AVAILABLE": "时移功能可用,点击播放", "LIVE_STREAM": "Live stream",
"EPG_NOT_AVAILABLE_DATE": "错误:电子节目单EPG在所选日期不可用", "TIMESHIFT_AVAILABLE": "时移功能可用,点击播放",
"EPG_NOT_AVAILABLE_CHANNEL_TITLE": "错误:电子节目单EPG不适用于此频道", "EPG_NOT_AVAILABLE_DATE": "错误:电子节目单EPG在所选日期不可用",
"EPG_NOT_AVAILABLE_CHANNEL_DESCRIPTION": "请添加/更改设置项中电子节目单的URL", "EPG_NOT_AVAILABLE_CHANNEL_TITLE": "错误:电子节目单EPG不适用于此频道",
"FETCH_EPG": "获取 EPG 数据...", "EPG_NOT_AVAILABLE_CHANNEL_DESCRIPTION": "请添加/更改设置项中电子节目单的URL",
"PROGRAM_DIALOG": { "FETCH_EPG": "获取 EPG 数据...",
"PROGRAM_DETAILS": "电视节目详情", "PROGRAM_DIALOG": {
"TITLE": "标题", "PROGRAM_DETAILS": "电视节目详情",
"CATEGORY": "类别", "TITLE": "标题",
"PARENTAL_RATING_SYSTEM": "电视内容分级系统", "CATEGORY": "类别",
"DESCRIPTION": "描述", "PARENTAL_RATING_SYSTEM": "电视内容分级系统",
"LANGUAGE": "语言", "DESCRIPTION": "描述",
"SHOW_PROGRAM_DETAILS": "显示有关此程序的详细信息" "LANGUAGE": "语言",
} "SHOW_PROGRAM_DETAILS": "显示有关此程序的详细信息"
}, }
"LANGUAGES": { },
"CHINESE": "简体中文", "LANGUAGES": {
"ENGLISH": "English", "CHINESE": "简体中文",
"KOREAN": "한국어", "ENGLISH": "English",
"RUSSIAN": "Русский", "KOREAN": "한국어",
"GERMAN": "Deutsch", "RUSSIAN": "Русский",
"SPANISH": "Español" "GERMAN": "Deutsch",
}, "SPANISH": "Español"
"YES": "是", },
"NO": "否" "YES": "是",
} "NO": "否"
}