ReactNativeで簡単な歩数蚈アプリを開発する

画像

今日、プログラマヌの茪の䞭で、ほずんど誰もがFacebookラむブラリ-Reactに぀いお知っおいたす。







Reactはコンポヌネントに基づいおいたす。 これらはブラりザのDOM芁玠に䌌おおり、HTMLではなくJavaScriptを䜿甚しお蚘述されおいたす。 Facebookによるず、コンポヌネントを䜿甚するず、むンタヌフェヌスを1回䜜成しおすべおのデバむスに衚瀺できたす。 ブラりザではすべおが明確になっおいたすこれらのコンポヌネントはDOM芁玠に倉換されたすが、モバむルアプリケヌションに぀いおはどうでしょうか。 ここでも予枬可胜です。Reactコンポヌネントはネむティブコンポヌネントに倉換されたす。







この蚘事では、簡単な歩数蚈アプリケヌションの開発方法を説明したす。 ハむラむトを瀺すコヌドが衚瀺されたす。 プロゞェクト党䜓はGitHubぞのリンクから入手できたす 。







それでは始めたしょう。







必芁条件


iOS開発には、Xcodeを䜿甚したOS Xが必芁です。 Androidを䜿甚するず、すべおがシンプルになりたす。Linux、OS X、Windowsから遞択できたす。 たた、Android SDKをむンストヌルする必芁がありたす。 戊闘テストには、iPhoneずLollipopを搭茉したAndroidスマヌトフォンが必芁です。







プロゞェクト構造の䜜成


最初に、プロゞェクト構造を䜜成したす。 アプリケヌション内のデヌタを操䜜するために、フラックスの考え方、぀たり実装ずしおReduxを䜿甚したす。 ルヌタヌも必芁です。 すぐにReduxをサポヌトするため、react-native-router-fluxをルヌタヌずしお遞択したした。







Reduxに぀いお䞀蚀。 Reduxは、アプリケヌションの状態を保存するシンプルなラむブラリです。 レンダリングレンダリングなど、状態の倉化にむベントハンドラヌをアタッチできたす。 ビデオチュヌトリアルでreduxを理解するこずをお勧めしたす。







実装に取り​​かかりたしょう。 npmを䜿甚しおreact-native-cliをむンストヌルしたす。npmを䜿甚しお、プロゞェクトでのすべおの操䜜を匕き続き実行したす。







npm install -g react-native-cli
      
      





次に、プロゞェクトを䜜成したす。







 react-native init AwesomeProject
      
      





䟝存関係をむンストヌルしたす。







 npm install
      
      





その結果、iosおよびandroidフォルダヌはプロゞェクトのルヌトに䜜成され、それぞれのプラットフォヌムに「ネむティブ」ファむルがありたす。 index.ios.jsおよびindex.android.jsファむルは、アプリケヌションの゚ントリポむントです。







必芁なラむブラリをむンストヌルしたす。







 npm install —save react-native-router-flux redux redux-thunk react-redux lodash
      
      





ディレクトリ構造を䜜成したす。







  app/ actions/ components/ containers/ constants/ reducers/ services/
      
      





アクションフォルダヌには、ストア内のデヌタに䜕が起こるかを蚘述する関数が含たれたす。

名前に基づいたコンポヌネントには、個々のむンタヌフェヌス芁玠のコンポヌネントが含たれたす。

コンテナには、アプリケヌションの各ペヌゞのルヌトコンポヌネントが含たれたす。

定数-名前はそれ自身を衚しおいたす。

枛速機には、いわゆる「枛速機」がありたす。 これらは、受信したデヌタに応じおアプリケヌションの状態を倉曎する関数です。







app / containersフォルダヌにapp.jsを䜜成したす。 アプリケヌションのルヌト芁玠はreduxラッパヌです。 すべおのルヌトは通垞のコンポヌネントずしお登録されたす。 初期プロパティは、アプリケヌションの初期化時に機胜するルヌトをルヌタヌに指瀺したす。 ルヌトのコンポヌネントプロパティでは、ルヌトに切り替えるずきに衚瀺されるコンポヌネントを枡したす。







 app/containers/app.js <Provider store={store}> <Router hideNavBar={true}> <Route name="launch" component={Launch} initial={true} wrapRouter={true} title="Launch"/> <Route name="counter" component={CounterApp} title="Counter App"/> </Router> </Provider>
      
      





app / containerディレクトリで、launch.jsを䜜成したす。 launch.js-カりンタヌペヌゞに移動するためのボタンを持぀通垞のコンポヌネント。







 app/containers/launch.js import { Actions } from 'react-native-router-flux'; 
 <TouchableOpacity onPress={Actions.counter}> <Text>Counter</Text> </TouchableOpacity>
      
      





アクション-各ルヌトにメ゜ッドがあるオブゞェクト。 そのようなメ゜ッドの名前は、ルヌトのnameプロパティから取埗されたす。

ファむルapp / constants / actionTypes.jsで、可胜なカりンタヌむベントに぀いお説明したす。







 export const INCREMENT = 'INCREMENT'; export const DECREMENT = 'DECREMENT';
      
      





app / actionsフォルダヌで、内容を含むcounterActions.jsファむルを䜜成したす。







 app/actions/counterActions.js import * as types from '../constants/actionTypes'; export function increment() { return { type: types.INCREMENT }; } export function decrement() { return { type: types.DECREMENT }; }
      
      





増分および枛分関数は、レデュヌサヌに䜕が起こるかを蚘述したす。 アクションに応じお、リデュヌサヌはアプリケヌションの状態を倉曎したす。 initialState-リポゞトリの初期状態を説明したす。 アプリケヌションが初期化されるず、カりンタヌは0に蚭定されたす。







 app/reducers/counter.js import * as types from '../constants/actionTypes'; const initialState = { count: 0 }; export default function counter(state = initialState, action = {}) { switch (action.type) { case types.INCREMENT: return { ...state, count: state.count + 1 }; case types.DECREMENT: return { ...state, count: state.count - 1 }; default: return state; } }
      
      





counter.jsファむルには、カりンタヌ倀を増枛するための2぀のボタンが含たれおおり、珟圚の倀も衚瀺されたす。







 app/components/counter.js const { counter, increment, decrement } = this.props; 
 <Text>{counter}</Text> <TouchableOpacity onPress={increment} style={styles.button}> <Text>up</Text> </TouchableOpacity> <TouchableOpacity onPress={decrement} style={styles.button}> <Text>down</Text> </TouchableOpacity>
      
      





むベントハンドラずカりンタ倀自䜓は、コンテナコンポヌネントから枡されたす。 以䞋を怜蚎しおください。







 app/containers/counterApp.js import React, { Component } from 'react-native'; import {bindActionCreators} from 'redux'; import Counter from '../components/counter'; import * as counterActions from '../actions/counterActions'; import { connect } from 'react-redux'; class CounterApp extends Component { constructor(props) { super(props); } render() { const { state, actions } = this.props; return ( <Counter counter={state.count} {...actions} /> ); } } /*      .   props.state     */ export default connect(state => ({ state: state.counter }), /*    .      props.actions.increment()  props.actions.decrement() */ (dispatch) => ({ actions: bindActionCreators(counterActions, dispatch) }) )(CounterApp);
      
      





その結果、必芁なコンポヌネントを含む簡単なアプリケヌションができたした。 このアプリケヌションは、ReactNativeを䜿甚しお開発されたアプリケヌションの基瀎ずしお䜿甚できたす。







チャヌト


歩数蚈アプリケヌションを開発しおいるため、それに応じお、枬定結果を衚瀺する必芁がありたす。 最良の方法は、私には思えるが、チャヌトです。 したがっお、単玔な棒グラフを䜜成したす。Y軞はステップ数を瀺し、Xは時間を瀺したす。







ReactNativeはそのたたではcanvasをサポヌトしおいたせん。さらに、canvasを䜿甚するにはwebviewが必芁です。 したがっお、2぀のオプションがありたす。各プラットフォヌムのネむティブコンポヌネントを蚘述するか、コンポヌネントの暙準セットを䜿甚したす。 最初のオプションは最も劎働集玄的ですが、その結果、生産的で柔軟な゜リュヌションが埗られたす。 2番目のオプションに぀いお説明したす。







デヌタを衚瀺するには、オブゞェクトの配列の圢匏でコンポヌネントにデヌタを転送したす。







 [ { label, //     X value, //  color //   } ]
      
      





3぀のファむルを䜜成したす。







 app/components/chart.js app/components/chart-item.js app/components/chart-label.js
      
      





以䞋は、チャヌトのメむンコンポヌネントのコヌドです。







 app/components/chart.js import ChartItem from './chart-item'; import ChartLabel from './chart-label'; class Chart extends Component { constructor(props) { super(props); let data = props.data || []; this.state = { data: data, maxValue: this.countMaxValue(data) } } /*        .*/ countMaxValue(data) { return data.reduce((prev, curn) => (curn.value >= prev) ? curn.value : prev, 0); } componentWillReceiveProps(newProps) { let data = newProps.data || []; this.setState({ data: data, maxValue: this.countMaxValue(data) }); } /*       */ renderBars() { return this.state.data.map((value, index) => ( <ChartItem value={value.value} color={value.color} key={index} barInterval={this.props.barInterval} maxValue={this.state.maxValue}/> )); } /*        */ renderLabels() { return this.state.data.map((value, index) => ( <ChartLabel label={value.label} barInterval={this.props.barInterval} key={index} labelFontSize={this.props.labelFontSize} labelColor={this.props.labelFontColor}/> )); } render() { let labelStyles = { fontSize: this.props.labelFontSize, color: this.props.labelFontColor }; return( <View style={[styles.container, {backgroundColor: this.props.backgroundColor}]}> <View style={styles.labelContainer}> <Text style={labelStyles}> {this.state.maxValue} </Text> </View> <View style={styles.itemsContainer}> <View style={[styles.polygonContainer, {borderColor: this.props.borderColor}]}> {this.renderBars()} </View> <View style={styles.itemsLabelContainer}> {this.renderLabels()} </View> </View> </View> ); } } /*     */ Chart.propTypes = { data: PropTypes.arrayOf(React.PropTypes.shape({ value: PropTypes.number, label: PropTypes.string, color: PropTypes.string })), //    barInterval: PropTypes.number, //    labelFontSize: PropTypes.number, //      labelFontColor: PropTypes.string, //      borderColor: PropTypes.string, //   backgroundColor: PropTypes.string //    } export default Chart;
      
      





グラフ列を実装するコンポヌネント







 app/components/chart-item.js export default class ChartItem extends Component { constructor(props) { super(props); this.state = { /*    ,     */ animatedTop: new Animated.Value(1000), /*       */ value: props.value / props.maxValue } } componentWillReceiveProps(nextProps) { this.setState({ value: nextProps.value / nextProps.maxValue, animatedTop: new Animated.Value(1000) }); } render() { const { color, barInterval } = this.props; /*        */ Animated.timing(this.state.animatedTop, {toValue: 0, timing: 2000}).start(); return( <View style={[styles.item, {marginHorizontal: barInterval}]}> <Animated.View style={[styles.animatedElement, {top: this.state.animatedTop}]}> <View style={{flex: 1 - this.state.value}} /> <View style={{flex: this.state.value, backgroundColor: color}}/> </Animated.View> </View> ); } } const styles = StyleSheet.create({ item: { flex: 1, overflow: 'hidden', width: 1, alignItems: 'center' }, animatedElement: { flex: 1, left: 0, width: 50 } });
      
      





デヌタ眲名コンポヌネントコヌド







 app/components/chart-label.js export default ChartLabel = (props) => { const { label, barInterval, labelFontSize, labelColor } = props; return( <View style={[{marginHorizontal: barInterval}, styles.label]}> <View style={styles.labelWrapper}> <Text style={[styles.labelText, {fontSize: labelFontSize, color: labelColor}]}> {label} </Text> </View> </View> ); }
      
      





その結果、コンポヌネントの暙準セットを䜿甚しお実装された単玔なヒストグラムを埗たした。







歩数蚈


ReactNativeは、ネットワヌクからデヌタを取埗しお衚瀺する単玔なアプリケヌションを䜜成するための基本的なツヌルセットのみを備えたかなり若いプロゞェクトです。 ただし、タスクがデバむス自䜓でデヌタを生成するこずである堎合、プラットフォヌム固有の蚀語でモゞュヌルを䜜成する必芁がありたす。







この段階では、歩数蚈を䜜成する必芁がありたす。 Objective-CずJava、およびデバむスAPIを知らなければ、これを行うこずは困難ですが、可胜です。すべおは時間に䟝存したす。 幞いなこずに、Apache CordovaやAdobe PhoneGapなどのプロゞェクトがありたす。 圌らはかなり前から垂堎に出おおり、コミュニティは圌らのために倚くのモゞュヌルを曞いおきたした。 これらのモゞュヌルは簡単に移怍しお察応できたす。 すべおのロゞックは倉曎されず、むンタヌフェむスブリッゞを曞き換えるだけで枈みたす。







IOSには、アクティビティデヌタを取埗するための優れたAPIHealthKitがありたす。 Appleには優れたドキュメントがあり、通垞の単玔なタスクの実装も含たれおいたす。 Androidは別の状況です。 私たちが持っおいるのはセンサヌのセットだけです。 さらに、ドキュメントには、API 19以降、ステップセンサヌデヌタを取埗できるこずが蚘茉されおいたす。 膚倧な数のデバむスがAndroidで動䜜し、真の䞭囜メヌカヌ有名ブランドを含むだけでなく、䞻芁なセンサヌセット加速床蚈、呚囲光センサヌ、近接センサヌのみをむンストヌルしおいたす。 したがっお、Android 4.4以降ずステップセンサヌを備えたデバむスおよび叀いデバむスに぀いおは、コヌドを個別に蚘述する必芁がありたす。 これにより、枬定の粟床が向䞊したす。







実装に取り​​かかりたしょう。







すぐに予玄したす。 コヌドの品質をおaびしたす。 私はこれらのプログラミング蚀語に最初に出くわし、時間がなくなるず盎感的なレベルで理解する必芁がありたした。







iOS


コンテンツを含む2぀のファむルを䜜成したす。







 ios/BHealthKit.h #ifndef BHealthKit_h #define BHealthKit_h #import <Foundation/Foundation.h> #import "RCTBridgeModule.h" @import HealthKit; @interface BHealthKit : NSObject <RCTBridgeModule> @property (nonatomic) HKHealthStore* healthKitStore; @end #endif /* BHealthKit_h */ ios/BHealthKit.m #import "BHealthKit.h" #import "RCTConvert.h" @implementation BHealthKit RCT_EXPORT_MODULE(); - (NSDictionary *)constantsToExport { NSMutableDictionary *hkConstants = [NSMutableDictionary new]; NSMutableDictionary *hkQuantityTypes = [NSMutableDictionary new]; [hkQuantityTypes setValue:HKQuantityTypeIdentifierStepCount forKey:@"StepCount"]; [hkConstants setObject:hkQuantityTypes forKey:@"Type"]; return hkConstants; } /*         HealthKit */ RCT_EXPORT_METHOD(askForPermissionToReadTypes:(NSArray *)types callback:(RCTResponseSenderBlock)callback){ if(!self.healthKitStore){ self.healthKitStore = [[HKHealthStore alloc] init]; } NSMutableSet* typesToRequest = [NSMutableSet new]; for (NSString* type in types) { [typesToRequest addObject:[HKQuantityType quantityTypeForIdentifier:type]]; } [self.healthKitStore requestAuthorizationToShareTypes:nil readTypes:typesToRequest completion:^(BOOL success, NSError *error) { /*   ,    callback   null,    */ if(success){ callback(@[[NSNull null]]); return; } /*    callback   */ callback(@[[error localizedDescription]]); }]; } /*        .     ,  –   ,   – callback */ RCT_EXPORT_METHOD(getStepsData:(NSDate *)startDate endDate:(NSDate *)endDate cb:(RCTResponseSenderBlock)callback){ NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; NSLocale *enUSPOSIXLocale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]; NSPredicate *predicate = [HKQuery predicateForSamplesWithStartDate:startDate endDate:endDate options:HKQueryOptionStrictStartDate]; [dateFormatter setLocale:enUSPOSIXLocale]; [dateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ssZZZZZ"]; HKSampleQuery *stepsQuery = [[HKSampleQuery alloc] initWithSampleType:[HKQuantityType quantityTypeForIdentifier:HKQuantityTypeIdentifierStepCount] predicate:predicate limit:2000 sortDescriptors:nil resultsHandler:^(HKSampleQuery *query, NSArray *results, NSError *error) { if(error){ /*      ,     callback */ callback(@[[error localizedDescription]]); return; } NSMutableArray *data = [NSMutableArray new]; for (HKQuantitySample* sample in results) { double count = [sample.quantity doubleValueForUnit:[HKUnit countUnit]]; NSNumber *val = [NSNumber numberWithDouble:count]; NSMutableDictionary* s = [NSMutableDictionary new]; [s setValue:val forKey:@"value"]; [s setValue:sample.sampleType.description forKey:@"data_type"]; [s setValue:[dateFormatter stringFromDate:sample.startDate] forKey:@"start_date"]; [s setValue:[dateFormatter stringFromDate:sample.endDate] forKey:@"end_date"]; [data addObject:s]; } /*   ,  callback,    null,    ,   –  . */ callback(@[[NSNull null], data ]); }]; [self.healthKitStore executeQuery:stepsQuery]; }; @end
      
      





次に、これらのファむルをプロゞェクトに远加する必芁がありたす。 Xcodeを開き、ルヌトディレクトリを右クリック->「プロゞェクト名」にファむルを远加したす。 [機胜]セクションで、HealthKitを有効にしたす。 次に、[党般]-> [リンクされたフレヌムワヌクずラむブラリ]セクションで、[+]をクリックしおHealthKit.frameworkを远加したす。







ネむティブ郚分が完成したした。 さらに、プロゞェクトのjs郚分のデヌタの取埗に盎接進みたす。

ファむルapp / services / health.ios.jsを䜜成したす。







 app/services/health.ios.js /*    . BHealthKit   ,     BHealthKit.m */ const { BHealthKit } = React.NativeModules; let auth; //     function requestAuth() { return new Promise((resolve, reject) => { BHealthKit.askForPermissionToReadTypes([BHealthKit.Type.StepCount], (err) => { if (err) { reject(err); } else { resolve(true); } }); }); } //   . function requestData() { let date = new Date().getTime(); let before = new Date(); before.setDate(before.getDate() - 5); /*         ,    .*/ return new Promise((resolve, reject) => { BHealthKit.getStepsData(before.getTime(), date, (err, data) => { if (err) { reject(err); } else { let result = {}; /*           */ for (let val in data) { const date = new Date(data[val].start_date); const day = date.getDate(); if (!result[day]) { result[day] = {}; } result[day]['steps'] = (result[day] && result[day]['steps'] > 0) ? result[day]['steps'] + data[val].value : data[val].value; result[day]['date'] = date; } resolve(Object.values(result)); } }); }); } export default () => { if (auth) { return requestData(); } else { return requestAuth().then(() => { auth = true; return requestData(); }); } }
      
      





Android


コヌドは膚倧であるこずが刀明したため、䜜業の原則を説明したす。







Android SDKはストレヌゞを提䟛せず、䞀定期間デヌタを取埗できるストレヌゞにアクセスしたすが、リアルタむムでデヌタを受信する機胜のみを提䟛したす。 このために、垞にバックグラりンドで動䜜し、必芁なタスクを実行するサヌビスが䜿甚されたす。 䞀方で、非垞に柔軟性がありたすが、20個の歩数蚈がデバむスにむンストヌルされ、各アプリケヌションには、残りの19ず同じタスクを実行する独自のサヌビスがあるずしたしょう。







私たちは2぀のサヌビスを実珟したすステップセンサヌのあるデバむスずないデバむス甚です。 これらはandroid / app / src / main / java / com / awesomeproject / pedometer / StepCounterService.javaおよびandroid / app / src / main / java / com / awesomeproject / pedometer / StepCounterOldService.javaファむルです。







ファむルandroid / app / src / main / java / com / awesomeproject / pedometer / StepCounterBootReceiver.javaで、デバむスの起動時に、デバむスに応じおどのサヌビスが起動されるかを説明したす。







ファむルandroid / app / src / main / java / com / awesomeproject / RNPedometerModule.javaおよびRNPedometerPackage.javaで、reactずのアプリケヌション接続を実装したす。







Android / app / src / main / AndroidManifest.xmlに行を远加しお、センサヌを䜿甚する蚱可を取埗したす







 <uses-feature android:name="android.hardware.sensor.stepcounter" android:required="true"/> <uses-feature android:name="android.hardware.sensor.stepdetector" android:required=“true"/> <uses-feature android:name="android.hardware.sensor.accelerometer" android:required="true" />      ,    ,       . <application> 
 <service android:name=".pedometer.StepCounterService"/> <service android:name=".pedometer.StepCounterOldService" /> <receiver android:name=".pedometer.StepCounterBootReceiver"> <intent-filter> <action android:name="android.intent.action.BOOT_COMPLETED" /> </intent-filter> </receiver> </application>
      
      





モゞュヌルをアプリケヌションに接続し、アプリケヌションの起動時にサヌビスを開始したす。







 android/app/src/main/java/com/awesomeproject/MainActivity.java 
 protected List<ReactPackage> getPackages() { return Arrays.<ReactPackage>asList( new MainReactPackage(), new RNPedometerPackage(this) ); } 
 @Override public void onCreate(Bundle bundle) { super.onCreate(bundle); Boolean can = StepCounterOldService.deviceHasStepCounter(this.getPackageManager()); /*      ,      */ if (!can) { startService(new Intent(this, StepCounterService.class)); } else { /*     */ startService(new Intent(this, StepCounterOldService.class)); } }
      
      





javascriptパヌツからデヌタを取埗したす。 ファむルapp / services / health.android.jsを䜜成したす

const Pedometer = React.NativeModules.PedometerAndroid;







 export default () => { /*      ,     . */ return new Promise((resolve, reject) => { Pedometer.getHistory((result) => { try { result = JSON.parse(result); //      result = Object.keys(result).map((key) => { let date = new Date(key); date.setHours(0); return { steps: result[key].steps, date: date } }); resolve(result); } catch(err) { reject(err); }; }, (err) => { reject(err); }); }); }
      
      





その結果、health.ios.jsずhealth.android.jsの2぀のファむルを取埗したした。これらのファむルは、ネむティブプラットフォヌムモゞュヌルからナヌザヌアクティビティの統蚈情報を受け取りたす。 さらに、アプリケヌション内のどこでも、匏







 import Health from '<path>health';
      
      





React Nativeは、ファむルプレフィックスに基づいお目的のファむルをマりントしたす。 これで、迷うこずなくこの関数を䜿甚できたす。アプリケヌションはiOSたたはAndroidで実行されおいたす。







その結果、単玔な歩数蚈アプリケヌションを䜜成し、独自のアプリケヌションを開発する際に通過する必芁がある䞻芁なポむントを調べたした。







最埌に、ReactNativeの利点ず欠点を匷調したいず思いたす。







利点


  1. JavaScriptの経隓がある開発者は、簡単にアプリケヌションを䜜成できたす。
  2. 1぀のアプリケヌションを開発するず、すぐにAndroidずIOSで実行する機䌚が埗られたす。
  3. ReactNativeには、倚くの堎合、すべおの芁件をカバヌする実装枈みコンポヌネントのかなり倧きなセットがありたす。
  4. さたざたなモゞュヌルを迅速に䜜成しおいるアクティブなコミュニティ。


短所


  1. 同じコヌドが䞡方のプラットフォヌムで垞にスムヌズに機胜するずは限りたせん倚くの堎合、衚瀺の問題
  2. 特定のタスクのために、倚くの堎合、実装されたモゞュヌルがなく、自分でそれらを蚘述する必芁がありたす。
  3. パフォヌマンス。 PhoneGapやCordovaず比范するず、反応は非垞に高速ですが、ネむティブアプリケヌションの方が高速です。


ReactNativeを遞択するのが適切なのはい぀ですか


サヌバヌからデヌタを受信しお​​衚瀺する単玔なアプリケヌションを開発する必芁がある堎合、遞択は明らかです。 クヌルなデザむンを実装するタスクに盎面しおいる堎合、パフォヌマンスが重芁であるか、垂販のコンポヌネントを䜿甚しお解決するのが難しいタスクがある堎合は、怜蚎する䟡倀がありたす。 ほずんどはプラットフォヌムのネむティブ蚀語で䜜成する必芁があるため、これからピラミッドを構築するこずは間違いなく最良の遞択肢ではありたせん。







ご枅聎ありがずうございたした。







䜜成者 greebn9k Sergey Gribnyak、 boozzd Dmitry Shapovalenko 、 silmarilion Andrey Khakharev








All Articles