├── .watchmanconfig ├── src ├── version.json ├── icon.png ├── DCTools.js ├── reducers │ ├── mods.js │ ├── modders.js │ ├── characters.js │ ├── installed.js │ ├── model-info.js │ ├── view.js │ ├── config.js │ ├── loading.js │ ├── history.js │ └── lists.js ├── lib │ ├── to-filename.js │ ├── model-info.js │ ├── swap.js │ ├── store.js │ ├── paths.js │ ├── update.js │ └── installed.js ├── actions │ ├── loading.js │ ├── installed.js │ ├── history.js │ ├── modders.js │ ├── view.js │ ├── characters.js │ ├── model-info.js │ ├── config.js │ ├── mods.js │ └── lists.js ├── ScrollTop.js ├── Modders.js ├── ScaledImage.js ├── CharacterPreview.js ├── Modder.js ├── ModLive2DPreview.js ├── ModPreview.js ├── Lists.js ├── Tools.js ├── Character.js ├── InstalledMods.js ├── Drawer.js ├── Variant.js ├── ModderCreditLink.js ├── EditList.js ├── Mods.js ├── Settings.js ├── InstalledPreview.js ├── Characters.js ├── List.js ├── MainView.js └── App.js ├── .gitattributes ├── app.json ├── .vscode └── settings.json ├── .eslintrc.js ├── babel.config.js ├── android-icon-48x48.png ├── android ├── app │ ├── debug.keystore │ ├── src │ │ ├── main │ │ │ ├── res │ │ │ │ ├── values │ │ │ │ │ ├── strings.xml │ │ │ │ │ └── styles.xml │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ └── mipmap-xxxhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ └── ic_launcher_round.png │ │ │ ├── assets │ │ │ │ └── fonts │ │ │ │ │ ├── Entypo.ttf │ │ │ │ │ ├── Feather.ttf │ │ │ │ │ ├── Fontisto.ttf │ │ │ │ │ ├── Ionicons.ttf │ │ │ │ │ ├── Octicons.ttf │ │ │ │ │ ├── Zocial.ttf │ │ │ │ │ ├── AntDesign.ttf │ │ │ │ │ ├── EvilIcons.ttf │ │ │ │ │ ├── Foundation.ttf │ │ │ │ │ ├── FontAwesome.ttf │ │ │ │ │ ├── MaterialIcons.ttf │ │ │ │ │ ├── SimpleLineIcons.ttf │ │ │ │ │ ├── FontAwesome5_Solid.ttf │ │ │ │ │ ├── FontAwesome5_Brands.ttf │ │ │ │ │ ├── FontAwesome5_Regular.ttf │ │ │ │ │ └── MaterialCommunityIcons.ttf │ │ │ ├── java │ │ │ │ └── com │ │ │ │ │ ├── dcmodmanager │ │ │ │ │ ├── MainActivity.java │ │ │ │ │ ├── CustomPackage.java │ │ │ │ │ └── MainApplication.java │ │ │ │ │ └── arsylk │ │ │ │ │ └── mammonsmite │ │ │ │ │ ├── DestinyChild │ │ │ │ │ ├── DCDefine.java │ │ │ │ │ ├── DCModelInfo.java │ │ │ │ │ └── Pck.java │ │ │ │ │ ├── Live2D │ │ │ │ │ └── L2DModel.java │ │ │ │ │ └── utils │ │ │ │ │ └── Utils.java │ │ │ └── AndroidManifest.xml │ │ └── debug │ │ │ ├── AndroidManifest.xml │ │ │ └── java │ │ │ └── com │ │ │ └── dcmodmanager │ │ │ └── ReactNativeFlipper.java │ ├── proguard-rules.pro │ ├── .project │ ├── build_defs.bzl │ └── BUCK ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── .project ├── settings.gradle ├── build.gradle ├── gradle.properties ├── gradlew.bat └── gradlew ├── screenshots ├── screenshot-1.jpg ├── screenshot-2.jpg ├── screenshot-3.jpg ├── screenshot-4.jpg ├── screenshot-5.jpg └── screenshot-6.jpg ├── ios ├── DcModManager │ ├── Images.xcassets │ │ ├── Contents.json │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── AppDelegate.h │ ├── main.m │ ├── AppDelegate.m │ ├── Info.plist │ └── Base.lproj │ │ └── LaunchScreen.xib ├── DcModManagerTests │ ├── Info.plist │ └── DcModManagerTests.m ├── DcModManager-tvOSTests │ └── Info.plist ├── DcModManager-tvOS │ └── Info.plist ├── DcModManager.xcodeproj │ └── xcshareddata │ │ └── xcschemes │ │ ├── DcModManager.xcscheme │ │ └── DcModManager-tvOS.xcscheme └── Podfile ├── .buckconfig ├── .prettierrc.js ├── index.js ├── __tests__ └── App-test.js ├── metro.config.js ├── scripts └── pre-build.js ├── .gitignore ├── package.json ├── .flowconfig └── README.md /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /src/version.json: -------------------------------------------------------------------------------- 1 | "0.0.14" -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.pbxproj -text 2 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "DcModManager", 3 | "displayName": "DC Mod Manager" 4 | } -------------------------------------------------------------------------------- /src/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhasmaExMachina/dc-mod-manager/HEAD/src/icon.png -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "java.configuration.updateBuildConfiguration": "automatic" 3 | } -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: '@react-native-community', 4 | }; 5 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['module:metro-react-native-babel-preset'], 3 | }; 4 | -------------------------------------------------------------------------------- /android-icon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhasmaExMachina/dc-mod-manager/HEAD/android-icon-48x48.png -------------------------------------------------------------------------------- /android/app/debug.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhasmaExMachina/dc-mod-manager/HEAD/android/app/debug.keystore -------------------------------------------------------------------------------- /screenshots/screenshot-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhasmaExMachina/dc-mod-manager/HEAD/screenshots/screenshot-1.jpg -------------------------------------------------------------------------------- /screenshots/screenshot-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhasmaExMachina/dc-mod-manager/HEAD/screenshots/screenshot-2.jpg -------------------------------------------------------------------------------- /screenshots/screenshot-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhasmaExMachina/dc-mod-manager/HEAD/screenshots/screenshot-3.jpg -------------------------------------------------------------------------------- /screenshots/screenshot-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhasmaExMachina/dc-mod-manager/HEAD/screenshots/screenshot-4.jpg -------------------------------------------------------------------------------- /screenshots/screenshot-5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhasmaExMachina/dc-mod-manager/HEAD/screenshots/screenshot-5.jpg -------------------------------------------------------------------------------- /screenshots/screenshot-6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhasmaExMachina/dc-mod-manager/HEAD/screenshots/screenshot-6.jpg -------------------------------------------------------------------------------- /android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | DC Mod Manager 3 | 4 | -------------------------------------------------------------------------------- /ios/DcModManager/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhasmaExMachina/dc-mod-manager/HEAD/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /.buckconfig: -------------------------------------------------------------------------------- 1 | 2 | [android] 3 | target = Google Inc.:Google APIs:23 4 | 5 | [maven_repositories] 6 | central = https://repo1.maven.org/maven2 7 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | bracketSpacing: false, 3 | jsxBracketSameLine: true, 4 | singleQuote: true, 5 | trailingComma: 'all', 6 | }; 7 | -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/Entypo.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhasmaExMachina/dc-mod-manager/HEAD/android/app/src/main/assets/fonts/Entypo.ttf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/Feather.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhasmaExMachina/dc-mod-manager/HEAD/android/app/src/main/assets/fonts/Feather.ttf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/Fontisto.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhasmaExMachina/dc-mod-manager/HEAD/android/app/src/main/assets/fonts/Fontisto.ttf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/Ionicons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhasmaExMachina/dc-mod-manager/HEAD/android/app/src/main/assets/fonts/Ionicons.ttf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/Octicons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhasmaExMachina/dc-mod-manager/HEAD/android/app/src/main/assets/fonts/Octicons.ttf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/Zocial.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhasmaExMachina/dc-mod-manager/HEAD/android/app/src/main/assets/fonts/Zocial.ttf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/AntDesign.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhasmaExMachina/dc-mod-manager/HEAD/android/app/src/main/assets/fonts/AntDesign.ttf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/EvilIcons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhasmaExMachina/dc-mod-manager/HEAD/android/app/src/main/assets/fonts/EvilIcons.ttf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/Foundation.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhasmaExMachina/dc-mod-manager/HEAD/android/app/src/main/assets/fonts/Foundation.ttf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/FontAwesome.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhasmaExMachina/dc-mod-manager/HEAD/android/app/src/main/assets/fonts/FontAwesome.ttf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/MaterialIcons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhasmaExMachina/dc-mod-manager/HEAD/android/app/src/main/assets/fonts/MaterialIcons.ttf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/SimpleLineIcons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhasmaExMachina/dc-mod-manager/HEAD/android/app/src/main/assets/fonts/SimpleLineIcons.ttf -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhasmaExMachina/dc-mod-manager/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhasmaExMachina/dc-mod-manager/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhasmaExMachina/dc-mod-manager/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/FontAwesome5_Solid.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhasmaExMachina/dc-mod-manager/HEAD/android/app/src/main/assets/fonts/FontAwesome5_Solid.ttf -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhasmaExMachina/dc-mod-manager/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhasmaExMachina/dc-mod-manager/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/FontAwesome5_Brands.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhasmaExMachina/dc-mod-manager/HEAD/android/app/src/main/assets/fonts/FontAwesome5_Brands.ttf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/FontAwesome5_Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhasmaExMachina/dc-mod-manager/HEAD/android/app/src/main/assets/fonts/FontAwesome5_Regular.ttf -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhasmaExMachina/dc-mod-manager/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhasmaExMachina/dc-mod-manager/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/MaterialCommunityIcons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhasmaExMachina/dc-mod-manager/HEAD/android/app/src/main/assets/fonts/MaterialCommunityIcons.ttf -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhasmaExMachina/dc-mod-manager/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhasmaExMachina/dc-mod-manager/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhasmaExMachina/dc-mod-manager/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /src/DCTools.js: -------------------------------------------------------------------------------- 1 | import { NativeModules } from 'react-native'; 2 | 3 | console.log('NativeModules', NativeModules, NativeModules.DCTools) 4 | 5 | module.exports = NativeModules.DCTools -------------------------------------------------------------------------------- /src/reducers/mods.js: -------------------------------------------------------------------------------- 1 | import { MODS_SET } from "../actions/mods" 2 | 3 | export default (state = {}, action) => { 4 | if(action.type == MODS_SET) { 5 | return action.mods 6 | } 7 | return state 8 | } -------------------------------------------------------------------------------- /src/reducers/modders.js: -------------------------------------------------------------------------------- 1 | import {MODDERS_SET} from "../actions/modders" 2 | 3 | export default (state = {}, action) => { 4 | if(action.type == MODDERS_SET) { 5 | return action.modders 6 | } 7 | return state 8 | } -------------------------------------------------------------------------------- /src/lib/to-filename.js: -------------------------------------------------------------------------------- 1 | export default name => 2 | name.replace(/[^a-zA-Z0-9\s]/g, '') 3 | .toLowerCase() 4 | .replace(/^\s*/, '') 5 | .replace(/\s*$/, '') 6 | .replace(/\s\s/g, ' ') 7 | .replace(/\s/g, '-') -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @format 3 | */ 4 | 5 | import {AppRegistry} from 'react-native'; 6 | import App from './src/App'; 7 | import {name as appName} from './app.json'; 8 | 9 | AppRegistry.registerComponent(appName, () => App); 10 | -------------------------------------------------------------------------------- /src/reducers/characters.js: -------------------------------------------------------------------------------- 1 | import {CHARACTERS_SET} from "../actions/characters" 2 | 3 | export default (state = {}, action) => { 4 | if(action.type == CHARACTERS_SET) { 5 | return action.characters 6 | } 7 | return state 8 | } -------------------------------------------------------------------------------- /src/reducers/installed.js: -------------------------------------------------------------------------------- 1 | import {INSTALLED_SET} from '../actions/installed' 2 | 3 | export default (state = {}, action) => { 4 | if(action.type == INSTALLED_SET) { 5 | return {...action.installed} 6 | } 7 | return state 8 | } -------------------------------------------------------------------------------- /src/reducers/model-info.js: -------------------------------------------------------------------------------- 1 | import { MODEL_INFO_SET } from "../actions/model-info" 2 | 3 | export default (state = {}, action) => { 4 | if(action.type == MODEL_INFO_SET) { 5 | return action.modelInfo 6 | } 7 | return state 8 | } -------------------------------------------------------------------------------- /ios/DcModManager/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface AppDelegate : UIResponder 5 | 6 | @property (nonatomic, strong) UIWindow *window; 7 | 8 | @end 9 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /ios/DcModManager/main.m: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | #import "AppDelegate.h" 4 | 5 | int main(int argc, char * argv[]) { 6 | @autoreleasepool { 7 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/reducers/view.js: -------------------------------------------------------------------------------- 1 | import {VIEW_SET} from '../actions/view' 2 | 3 | export default (state = {}, action) => { 4 | if(action.type == VIEW_SET) { 5 | return { 6 | name: action.name, 7 | data: action.data 8 | } 9 | } 10 | return state 11 | } -------------------------------------------------------------------------------- /src/actions/loading.js: -------------------------------------------------------------------------------- 1 | export const LOADING_SET_LOADING = 'LOADING_SET_LOADING' 2 | 3 | export const setLoading = (isLoading, {title = 'Loading', message = '', progress = 0, total = 0} = {}) => ({ 4 | type: LOADING_SET_LOADING, 5 | isLoading, title, message, progress, total 6 | }) -------------------------------------------------------------------------------- /src/reducers/config.js: -------------------------------------------------------------------------------- 1 | import {CONFIG_SET} from '../actions/config' 2 | 3 | const defaultConfig = {region: 'global'} 4 | 5 | export default (state = defaultConfig, action) => { 6 | if(action.type === CONFIG_SET) { 7 | return {...state, ...action.config} 8 | } 9 | return state 10 | } -------------------------------------------------------------------------------- /src/reducers/loading.js: -------------------------------------------------------------------------------- 1 | import {LOADING_SET_LOADING} from '../actions/loading' 2 | 3 | export default (state = {}, action) => { 4 | if(action.type == LOADING_SET_LOADING) { 5 | const loading = Object.assign({}, action) 6 | delete loading.type 7 | return loading 8 | } 9 | return state 10 | } -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /__tests__/App-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @format 3 | */ 4 | 5 | import 'react-native'; 6 | import React from 'react'; 7 | import App from '../App'; 8 | 9 | // Note: test renderer must be required after react-native. 10 | import renderer from 'react-test-renderer'; 11 | 12 | it('renders correctly', () => { 13 | renderer.create(); 14 | }); 15 | -------------------------------------------------------------------------------- /src/actions/installed.js: -------------------------------------------------------------------------------- 1 | import {readInstalled} from '../lib/installed' 2 | 3 | export const INSTALLED_SET = 'INSTALLED_SET' 4 | 5 | export const loadInstalled = () => 6 | dispatch => { 7 | readInstalled().then(installed => { 8 | dispatch({ 9 | type: INSTALLED_SET, 10 | installed 11 | }) 12 | }) 13 | } 14 | 15 | -------------------------------------------------------------------------------- /metro.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Metro configuration for React Native 3 | * https://github.com/facebook/react-native 4 | * 5 | * @format 6 | */ 7 | 8 | module.exports = { 9 | transformer: { 10 | getTransformOptions: async () => ({ 11 | transform: { 12 | experimentalImportSupport: false, 13 | inlineRequires: false, 14 | }, 15 | }), 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /src/actions/history.js: -------------------------------------------------------------------------------- 1 | import {setView} from './view' 2 | export const HISTORY_POP = 'HISTORY_POP' 3 | 4 | export const popHistory = (history) => 5 | dispatch => { 6 | if(history.length > 1) { 7 | const previousView = history[history.length - 2] 8 | dispatch({type: HISTORY_POP}) 9 | dispatch(setView(previousView.name, previousView.data)) 10 | } 11 | } -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/reducers/history.js: -------------------------------------------------------------------------------- 1 | import {VIEW_PUSH} from '../actions/view' 2 | import {HISTORY_POP} from '../actions/history' 3 | import view from './view' 4 | 5 | export default (state = [], action) => { 6 | if(action.type == VIEW_PUSH) { 7 | state.push({name: action.name, data: action.data}) 8 | } 9 | if(action.type == HISTORY_POP) { 10 | return state.slice(0, state.length - 1) 11 | } 12 | return state 13 | } -------------------------------------------------------------------------------- /src/actions/modders.js: -------------------------------------------------------------------------------- 1 | export const MODDERS_SET = 'MODDERS_SET' 2 | 3 | export const fetchModders = () => 4 | (dispatch) => fetch('https://raw.githubusercontent.com/PhasmaExMachina/destiny-child-mods-archive/master/docs/data/modders.json') 5 | .then(response => response.json()) 6 | .then(data => dispatch(setModders(data))) 7 | .catch(e => console.log("ERROR LOADING MODDERS", e)) 8 | 9 | export const setModders = modders => ({type: MODDERS_SET, modders}) -------------------------------------------------------------------------------- /src/actions/view.js: -------------------------------------------------------------------------------- 1 | import {scrollToTop} from '../ScrollTop' 2 | 3 | export const VIEW_PUSH = 'VIEW_PUSH' 4 | export const VIEW_SET = 'VIEW_SET' 5 | 6 | export const pushView = (name, data = {}) => { 7 | return dispatch => { 8 | dispatch({type: VIEW_PUSH, name, data}) 9 | dispatch(setView(name, data)) 10 | } 11 | } 12 | 13 | export const setView = (name, data = {}) => { 14 | scrollToTop() 15 | return ({type: VIEW_SET, name, data}) 16 | } -------------------------------------------------------------------------------- /android/app/src/main/java/com/dcmodmanager/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.dcmodmanager; 2 | 3 | import com.facebook.react.ReactActivity; 4 | 5 | public class MainActivity extends ReactActivity { 6 | 7 | /** 8 | * Returns the name of the main component registered from JavaScript. This is used to schedule 9 | * rendering of the component. 10 | */ 11 | @Override 12 | protected String getMainComponentName() { 13 | return "DcModManager"; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/actions/characters.js: -------------------------------------------------------------------------------- 1 | export const CHARACTERS_SET = 'CHARACTERS_SET' 2 | 3 | export const fetchCharacters = () => 4 | (dispatch) => fetch('https://phasmaexmachina.github.io/destiny-child-mods-archive/data/characters.json') 5 | .then(response => response.json()) 6 | .then(data => {dispatch(setCharacters(data))}) 7 | .catch(e => console.log('Error fetching characters.json')) 8 | 9 | export const setCharacters = characters => ({type: CHARACTERS_SET, characters}) -------------------------------------------------------------------------------- /android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | -------------------------------------------------------------------------------- /src/actions/model-info.js: -------------------------------------------------------------------------------- 1 | export const MODEL_INFO_SET = 'MODEL_INFO_SET' 2 | 3 | export const fetchModelInfo = () => 4 | (dispatch) => { 5 | fetch('https://raw.githubusercontent.com/PhasmaExMachina/destiny-child-mods-archive/master/docs/data/model_info.merged.json') 6 | .then(response => response.json()) 7 | .then(data => { 8 | dispatch(setModelInfo(data)) 9 | }) 10 | } 11 | 12 | export const setModelInfo = modelInfo => { 13 | return {type: MODEL_INFO_SET, modelInfo} 14 | } -------------------------------------------------------------------------------- /android/app/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | app 4 | Project app created by Buildship. 5 | 6 | 7 | 8 | 9 | org.eclipse.buildship.core.gradleprojectbuilder 10 | 11 | 12 | 13 | 14 | 15 | org.eclipse.buildship.core.gradleprojectnature 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/lib/model-info.js: -------------------------------------------------------------------------------- 1 | import RNFS from 'react-native-fs' 2 | import {getModelInfoPath} from './paths' 3 | import store from './store' 4 | import modelInfo from '../reducers/model-info'; 5 | import RNFetchBlob from 'rn-fetch-blob' 6 | 7 | export const readModelInfo = () => 8 | RNFS.readFile(getModelInfoPath()) 9 | .then(localModelInfo => JSON.parse(localModelInfo), 'utf8') 10 | 11 | export const writeModelInfo = modelInfo => 12 | RNFetchBlob.fs.writeFile(getModelInfoPath(), JSON.stringify(modelInfo, null, 2), 'utf8') -------------------------------------------------------------------------------- /android/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | DC Mod Manager 4 | Project android created by Buildship. 5 | 6 | 7 | 8 | 9 | org.eclipse.buildship.core.gradleprojectbuilder 10 | 11 | 12 | 13 | 14 | 15 | org.eclipse.buildship.core.gradleprojectnature 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/ScrollTop.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {ScrollView, View} from 'react-native'; 3 | 4 | let ref 5 | 6 | function MainScrollView({children}) { 7 | return ( 8 | ref = v}> 9 | 10 | {children} 11 | 12 | 13 | ) 14 | } 15 | 16 | export default MainScrollView 17 | 18 | export const scrollToTop = () => { 19 | ref.scrollTo({y: 0}) 20 | setTimeout(() => { 21 | ref.scrollTo({y: 0}) 22 | }, 100) 23 | } -------------------------------------------------------------------------------- /src/reducers/lists.js: -------------------------------------------------------------------------------- 1 | import {LISTS_SET, LISTS_SET_ACTIVE, LISTS_COMMUNITY_SET} from "../actions/lists" 2 | 3 | export default (state = {lists:[], community: {}}, action) => { 4 | if(action.type == LISTS_SET) { 5 | state = {...state, lists: action.lists} 6 | } 7 | if(action.type == LISTS_SET_ACTIVE) { 8 | state = {...state, active: action.list} 9 | } 10 | if(action.type == LISTS_COMMUNITY_SET) { 11 | const {community} = state 12 | community[action.listName] = action.list 13 | state = {...state, community: {...community}} 14 | } 15 | return state 16 | } -------------------------------------------------------------------------------- /scripts/pre-build.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'), 2 | path = require('path'), 3 | existingVersion = require('../src/version.json'), 4 | {execSync} = require('child_process') 5 | 6 | const versionName = fs.readFileSync(path.join(__dirname, '../android/app/build.gradle'), 'utf8').match(/versionName "(\d+\.\d+\.\d+)"/)[1] 7 | if(existingVersion !== versionName) { 8 | fs.writeFileSync(path.join(__dirname, '../src/version.json'), JSON.stringify(versionName)) 9 | execSync(`git add src/version.json && git commit -m "Set app version to v${versionName}"`, 10 | {stdio: 'inherit'} 11 | ) 12 | } -------------------------------------------------------------------------------- /android/app/build_defs.bzl: -------------------------------------------------------------------------------- 1 | """Helper definitions to glob .aar and .jar targets""" 2 | 3 | def create_aar_targets(aarfiles): 4 | for aarfile in aarfiles: 5 | name = "aars__" + aarfile[aarfile.rindex("/") + 1:aarfile.rindex(".aar")] 6 | lib_deps.append(":" + name) 7 | android_prebuilt_aar( 8 | name = name, 9 | aar = aarfile, 10 | ) 11 | 12 | def create_jar_targets(jarfiles): 13 | for jarfile in jarfiles: 14 | name = "jars__" + jarfile[jarfile.rindex("/") + 1:jarfile.rindex(".jar")] 15 | lib_deps.append(":" + name) 16 | prebuilt_jar( 17 | name = name, 18 | binary_jar = jarfile, 19 | ) 20 | -------------------------------------------------------------------------------- /ios/DcModManager/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | } 33 | ], 34 | "info" : { 35 | "version" : 1, 36 | "author" : "xcode" 37 | } 38 | } -------------------------------------------------------------------------------- /src/lib/swap.js: -------------------------------------------------------------------------------- 1 | import RNFS from 'react-native-fs' 2 | import DCTools from '../DCTools' 3 | import store from './store' 4 | import {getModelInfoPath} from './paths' 5 | import {readModelInfo, writeModelInfo} from './model-info' 6 | 7 | export default (source, target, sourceCode, targetCode, hash) => { 8 | DCTools.swap(source, target) 9 | const {modelInfo, mods, config: {region}} = store.getState() 10 | if(modelInfo[sourceCode] && modelInfo[targetCode]) { 11 | return readModelInfo() 12 | .then(localModelInfo => { 13 | if(localModelInfo[targetCode]) { 14 | localModelInfo[targetCode] = mods[hash].modelInfo || modelInfo[sourceCode] 15 | return writeModelInfo(localModelInfo) 16 | } 17 | }) 18 | } 19 | else return Promise.resolve(new Promise(resolve => resolve())) 20 | } -------------------------------------------------------------------------------- /ios/DcModManagerTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/lib/store.js: -------------------------------------------------------------------------------- 1 | import {combineReducers, createStore, applyMiddleware} from 'redux' 2 | import thunk from 'redux-thunk' 3 | import characters from '../reducers/characters' 4 | import history from '../reducers/history' 5 | import lists from '../reducers/lists' 6 | import mods from '../reducers/mods' 7 | import modders from '../reducers/modders' 8 | import modelInfo from '../reducers/model-info' 9 | import loading from '../reducers/loading' 10 | import view from '../reducers/view' 11 | import installed from '../reducers/installed' 12 | import config from '../reducers/config' 13 | 14 | const store = createStore(combineReducers({ 15 | characters, 16 | mods, 17 | view, 18 | history, 19 | lists, 20 | modelInfo, 21 | modders, 22 | config, 23 | loading, 24 | installed 25 | }), applyMiddleware(thunk)); 26 | 27 | export default store -------------------------------------------------------------------------------- /ios/DcModManager-tvOSTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/dcmodmanager/CustomPackage.java: -------------------------------------------------------------------------------- 1 | package com.dcmodmanager; 2 | 3 | import com.facebook.react.ReactPackage; 4 | import com.facebook.react.bridge.NativeModule; 5 | import com.facebook.react.bridge.ReactApplicationContext; 6 | import com.facebook.react.uimanager.ViewManager; 7 | 8 | import java.util.ArrayList; 9 | import java.util.Collections; 10 | import java.util.List; 11 | 12 | public class CustomPackage implements ReactPackage { 13 | 14 | @Override 15 | public List createViewManagers(ReactApplicationContext reactContext) { 16 | return Collections.emptyList(); 17 | } 18 | 19 | @Override 20 | public List createNativeModules( 21 | ReactApplicationContext reactContext) { 22 | List modules = new ArrayList<>(); 23 | 24 | modules.add(new DCTools(reactContext)); 25 | 26 | return modules; 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /src/Modders.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {connect} from 'react-redux' 3 | import {View, TouchableHighlight} from 'react-native' 4 | import {Subheading, useTheme, Headline, Text, Button} from 'react-native-paper' 5 | import {pushView} from './actions/view' 6 | import ModPreview from './ModPreview' 7 | import InstalledPreview from './InstalledPreview' 8 | 9 | const Modders = ({modders, pushView}) => { 10 | return modders ? ( 11 | 12 | 13 | Modders 14 | 15 | {Object.keys(modders).sort().map(modder => 16 | 19 | )} 20 | 21 | ) : Loading modders ... 22 | } 23 | 24 | export default connect( 25 | ({modders}) => ({modders}), 26 | {pushView} 27 | )(Modders) -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'DcModManager' 2 | include ':react-native-apk-installer-n' 3 | project(':react-native-apk-installer-n').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-apk-installer-n/android') 4 | include ':rn-fetch-blob' 5 | project(':rn-fetch-blob').projectDir = new File(rootProject.projectDir, '../node_modules/rn-fetch-blob/android') 6 | include ':rn-fetch-blob' 7 | project(':rn-fetch-blob').projectDir = new File(rootProject.projectDir, '../node_modules/rn-fetch-blob/android') 8 | include ':react-native-vector-icons' 9 | project(':react-native-vector-icons').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-vector-icons/android') 10 | apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings) 11 | include ':app' 12 | include ':RNSendIntentModule', ':app' 13 | project(':RNSendIntentModule').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-send-intent/android') -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Xcode 6 | # 7 | build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata 17 | *.xccheckout 18 | *.moved-aside 19 | DerivedData 20 | *.hmap 21 | *.ipa 22 | *.xcuserstate 23 | 24 | # Android/IntelliJ 25 | # 26 | build/ 27 | .idea 28 | .gradle 29 | local.properties 30 | *.iml 31 | 32 | # node.js 33 | # 34 | node_modules/ 35 | npm-debug.log 36 | yarn-error.log 37 | 38 | # BUCK 39 | buck-out/ 40 | \.buckd/ 41 | *.keystore 42 | !debug.keystore 43 | 44 | # fastlane 45 | # 46 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 47 | # screenshots whenever they are needed. 48 | # For more information about the recommended setup visit: 49 | # https://docs.fastlane.tools/best-practices/source-control/ 50 | 51 | */fastlane/report.xml 52 | */fastlane/Preview.html 53 | */fastlane/screenshots 54 | 55 | # Bundle artifact 56 | *.jsbundle 57 | 58 | # CocoaPods 59 | /ios/Pods/ 60 | 61 | _notes.txt -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | ext { 5 | buildToolsVersion = "28.0.3" 6 | minSdkVersion = 16 7 | compileSdkVersion = 28 8 | targetSdkVersion = 28 9 | } 10 | repositories { 11 | google() 12 | jcenter() 13 | } 14 | dependencies { 15 | classpath("com.android.tools.build:gradle:3.5.2") 16 | 17 | // NOTE: Do not place your application dependencies here; they belong 18 | // in the individual module build.gradle files 19 | } 20 | } 21 | 22 | allprojects { 23 | repositories { 24 | mavenLocal() 25 | maven { 26 | // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm 27 | url("$rootDir/../node_modules/react-native/android") 28 | } 29 | maven { 30 | // Android JSC is installed from npm 31 | url("$rootDir/../node_modules/jsc-android/dist") 32 | } 33 | 34 | google() 35 | jcenter() 36 | maven { url 'https://www.jitpack.io' } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/ScaledImage.js: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useState} from 'react' 2 | import {Image} from 'react-native' 3 | import resolveAssetSource from 'react-native/Libraries/Image/resolveAssetSource' 4 | 5 | function ScaledImage(props) { 6 | const [width, setWidth] = useState(props.width || 0), 7 | [height, setHeight] = useState(props.height || 0) 8 | const setImageSize = (width, height) =>{ 9 | if(props.width && !props.height) { 10 | setHeight(height * (props.width / width)) 11 | } else if (!props.width && props.height) { 12 | setWidth(width * (props.height / height)) 13 | } 14 | } 15 | useEffect(() => { 16 | if(props.source.uri) { 17 | Image.getSize(props.source.uri, (width, height) => setImageSize(width, height)) 18 | } else { 19 | let {width, height} = resolveAssetSource(props.source) 20 | setImageSize(width, height) 21 | } 22 | }, []) 23 | 24 | const getStyles = () => { 25 | let styles = [{height, width}] 26 | if (props.style) styles.push(props.style) 27 | return styles 28 | } 29 | return (width && height) 30 | ? 31 | : null 32 | } 33 | 34 | export default ScaledImage -------------------------------------------------------------------------------- /src/lib/paths.js: -------------------------------------------------------------------------------- 1 | import RNFS from 'react-native-fs' 2 | import store from './store' 3 | 4 | const regions = { 5 | global: 'com.linegames.dcglobal', 6 | kr: 'com.NextFloor.DestinyChild', 7 | jp: 'com.stairs.destinychild' 8 | } 9 | 10 | export const getDcModManagerFolderPath = () => { 11 | return RNFS.ExternalStorageDirectoryPath + `/DCModManager/` 12 | } 13 | 14 | export const getDestinyChildPath = regionOverride => { 15 | const { config: { region } } = store.getState() 16 | return RNFS.ExternalStorageDirectoryPath + `/Android/data/${regions[regionOverride || region]}/` 17 | } 18 | 19 | export const getCharactersPath = () => { 20 | return getDestinyChildPath() + 'files/asset/character/' 21 | } 22 | 23 | export const getModelInfoPath = () => { 24 | return getCharactersPath() + 'model_info.json' 25 | } 26 | 27 | export const getListsPath = () => { 28 | return getDcModManagerFolderPath() + 'lists/' 29 | } 30 | 31 | export const getSettingsPath = () => { 32 | return getDcModManagerFolderPath() + '/config.json' 33 | } 34 | 35 | export const getInstalledPath = () => { 36 | const region = store.getState().config.region 37 | return getDcModManagerFolderPath() + `/installed${region}.json` 38 | } -------------------------------------------------------------------------------- /src/CharacterPreview.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {Card, Paragraph} from 'react-native-paper' 3 | import ScaledImage from './ScaledImage' 4 | import {pushView} from './actions/view' 5 | import {connect} from 'react-redux' 6 | 7 | const CharacterPreview = ({character, pushView}) => { 8 | const variant = ['02', '01', '00', '89', '10'].reduce((acc, v) => 9 | acc || (character.variants[v] ? v : false) 10 | , false) 11 | return ( 12 | pushView('character', {code: character.code})}> 13 | 14 | 15 | {character.variants[variant] && 16 | 20 | } 21 | 22 | {/* */} 23 | 24 | ) 25 | } 26 | 27 | export default connect( 28 | null, 29 | {pushView} 30 | )(CharacterPreview) -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | 20 | # AndroidX package structure to make it clearer which packages are bundled with the 21 | # Android operating system, and which are packaged with your app's APK 22 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 23 | android.useAndroidX=true 24 | # Automatically convert third-party libraries to use AndroidX 25 | android.enableJetifier=true 26 | 27 | # Version of flipper SDK to use with React Native 28 | FLIPPER_VERSION=0.33.1 29 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 14 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "DcModManager", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "android": "react-native run-android", 7 | "ios": "react-native run-ios", 8 | "start": "react-native start", 9 | "test": "jest", 10 | "lint": "eslint .", 11 | "build": "node ./scripts/pre-build.js && cd android && ./gradlew assembleRelease && cd .." 12 | }, 13 | "dependencies": { 14 | "@react-native-community/slider": "^3.0.0", 15 | "react": "16.11.0", 16 | "react-native": "0.62.2", 17 | "react-native-apk-installer-n": "^2.1.1", 18 | "react-native-breadcrumb": "^1.2.0", 19 | "react-native-fs": "^2.16.6", 20 | "react-native-paper": "^3.10.1", 21 | "react-native-permissions": "^2.1.5", 22 | "react-native-simple-toast": "^1.1.2", 23 | "react-native-vector-icons": "^6.6.0", 24 | "react-native-webview": "^10.4.1", 25 | "react-redux": "^7.2.0", 26 | "redux": "^4.0.5", 27 | "redux-thunk": "^2.3.0", 28 | "rn-fetch-blob": "^0.12.0" 29 | }, 30 | "devDependencies": { 31 | "@babel/core": "^7.10.3", 32 | "@babel/runtime": "^7.10.3", 33 | "@react-native-community/eslint-config": "^2.0.0", 34 | "babel-jest": "^26.1.0", 35 | "eslint": "^7.3.1", 36 | "jest": "^26.1.0", 37 | "metro-react-native-babel-preset": "^0.59.0", 38 | "react-test-renderer": "16.11.0" 39 | }, 40 | "jest": { 41 | "preset": "react-native" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/arsylk/mammonsmite/DestinyChild/DCDefine.java: -------------------------------------------------------------------------------- 1 | package com.arsylk.mammonsmite.DestinyChild; 2 | 3 | import java.util.regex.Pattern; 4 | 5 | public final class DCDefine { 6 | //first 8 bytes in pck files 7 | public static final byte[] 8 | PCK_IDENTIFIER = new byte[] {(byte)0x50, (byte)0x43, (byte)0x4B, (byte)0x00, (byte)0xCD, (byte)0xCC, (byte)0xCC, (byte)0x3E}; 9 | 10 | //first 4 bytes in hash 11 | public static final byte[] 12 | HASH_MODEL_OR_TEXTURE = new byte[] {(byte)0x66, (byte)0x0E, (byte)0x00, (byte)0x26}, 13 | HASH_CHARACTER = new byte[] {(byte)0x05, (byte)0x0E, (byte)0x00, (byte)0x25}, 14 | HASH_IDLE = new byte[] {(byte)0x28, (byte)0x0F, (byte)0x00, (byte)0x28}, 15 | HASH_ATTACK = new byte[] {(byte)0xEA, (byte)0x0F, (byte)0x00, (byte)0x2A}; 16 | public static final byte[] 17 | HASH_LOCALE_MODELIDS = new byte[] {(byte)0x30, (byte)0x0f, (byte)0x00, (byte)0x24}; 18 | 19 | //extensions 20 | public static final int UNKNOWN = 0, DAT = 1, MTN = 2, PNG = 3, JSON = 4, LOCALE_DEF = 12, LOCALE_TAB = 11; 21 | 22 | //patterns 23 | public static final Pattern LOCALE_DEF_LINE_PATTERN = Pattern.compile("^(?!/)(\\S+)\\s*?=\\s*?\"(.*?)\"\\s*?$"); 24 | public static final Pattern LOCALE_TAB_LINE_PATTERN = Pattern.compile("^(?!/)(\\S+)\\s(.*?)$"); 25 | public static final Pattern MODEL_ID_PATTERN = Pattern.compile("^.*\\d{3}_\\d{2}$"); 26 | } 27 | -------------------------------------------------------------------------------- /android/app/BUCK: -------------------------------------------------------------------------------- 1 | # To learn about Buck see [Docs](https://buckbuild.com/). 2 | # To run your application with Buck: 3 | # - install Buck 4 | # - `npm start` - to start the packager 5 | # - `cd android` 6 | # - `keytool -genkey -v -keystore keystores/debug.keystore -storepass android -alias androiddebugkey -keypass android -dname "CN=Android Debug,O=Android,C=US"` 7 | # - `./gradlew :app:copyDownloadableDepsToLibs` - make all Gradle compile dependencies available to Buck 8 | # - `buck install -r android/app` - compile, install and run application 9 | # 10 | 11 | load(":build_defs.bzl", "create_aar_targets", "create_jar_targets") 12 | 13 | lib_deps = [] 14 | 15 | create_aar_targets(glob(["libs/*.aar"])) 16 | 17 | create_jar_targets(glob(["libs/*.jar"])) 18 | 19 | android_library( 20 | name = "all-libs", 21 | exported_deps = lib_deps, 22 | ) 23 | 24 | android_library( 25 | name = "app-code", 26 | srcs = glob([ 27 | "src/main/java/**/*.java", 28 | ]), 29 | deps = [ 30 | ":all-libs", 31 | ":build_config", 32 | ":res", 33 | ], 34 | ) 35 | 36 | android_build_config( 37 | name = "build_config", 38 | package = "com.dcmodmanager", 39 | ) 40 | 41 | android_resource( 42 | name = "res", 43 | package = "com.dcmodmanager", 44 | res = "src/main/res", 45 | ) 46 | 47 | android_binary( 48 | name = "app", 49 | keystore = "//android/keystores:debug", 50 | manifest = "src/main/AndroidManifest.xml", 51 | package_type = "debug", 52 | deps = [ 53 | ":app-code", 54 | ], 55 | ) 56 | -------------------------------------------------------------------------------- /src/lib/update.js: -------------------------------------------------------------------------------- 1 | import store from '../lib/store' 2 | import {setLoading} from '../actions/loading' 3 | import currentVersion from '../version.json' 4 | import RNFetchBlob from "rn-fetch-blob" 5 | import RNApkInstallerN from 'react-native-apk-installer-n' 6 | 7 | export const checkForUpdate = () => fetch('https://github.com/PhasmaExMachina/dc-mod-manager/releases') 8 | .then(response => response.text()) 9 | .then(body => { 10 | const [_, version] = body.match(/dcmodmanager-v(\d+\.\d+\.\d+).apk/) || [] 11 | let changelog = [] 12 | // console.log(body.match(/markdown-body(.|\n)*?/)) 13 | try { 14 | changelog = body.match(/markdown-body(.|\n)*?
    (.|\n)*?<\/ul>/)[0].toString() 15 | .match(/
  • (.|\n)*?<\/li>/g) 16 | .map(t => t.replace(/<\/?li>/g, '')) 17 | } 18 | catch(e) {} 19 | return currentVersion !== version ? {version, changelog} : false; 20 | }) 21 | 22 | export const installUpdate = version => { 23 | store.dispatch(setLoading(true, {title: `Installing update`, message: `Downloading ... 0%`})) 24 | return RNFetchBlob.config({fileCache : true}) 25 | .fetch('GET', `https://github.com/PhasmaExMachina/dc-mod-manager/releases/download/v${version}/dcmodmanager-v${version}.apk`) 26 | .progress((received, total) => { 27 | store.dispatch(setLoading(true, {title: `Installing update`, message: `Downloading ... ${Math.round((received / total) * 100)}%`})) 28 | }) 29 | .then(res => { 30 | RNApkInstallerN.install(res.path()) 31 | store.dispatch(setLoading(false)) 32 | }) 33 | } -------------------------------------------------------------------------------- /src/Modder.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {connect} from 'react-redux' 3 | import {View, TouchableHighlight} from 'react-native' 4 | import {Subheading, useTheme, Headline, Text} from 'react-native-paper' 5 | import {pushView} from './actions/view' 6 | import ModPreview from './ModPreview' 7 | import InstalledPreview from './InstalledPreview' 8 | 9 | function Modder({modder, pushView, code, installed, mods}) { 10 | const {colors} = useTheme(), 11 | modderMods = Object.keys(mods).reduce((acc, hash) => { 12 | if(mods[hash].modder == modder) { 13 | acc.push(Object.assign({}, mods[hash], {hash})) 14 | } 15 | return acc 16 | }, []) 17 | return modder 18 | ? ( 19 | 20 | 21 | pushView('modders')}> 22 | Modders 23 | 24 | > 25 | {modder} 26 | 27 | 28 | {modder} - {modderMods.length} mods 29 | 30 | {modderMods.map(({hash, code, variant}) => 31 | 32 | )} 33 | 34 | ) 35 | : null 36 | } 37 | 38 | export default connect( 39 | ({modder, mods, view}) => ({ 40 | modder: view.data.modder, 41 | mods 42 | }), 43 | {pushView} 44 | )(Modder) -------------------------------------------------------------------------------- /ios/DcModManager-tvOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | NSAppTransportSecurity 26 | 27 | NSExceptionDomains 28 | 29 | localhost 30 | 31 | NSExceptionAllowsInsecureHTTPLoads 32 | 33 | 34 | 35 | 36 | NSLocationWhenInUseUsageDescription 37 | 38 | UILaunchStoryboardName 39 | LaunchScreen 40 | UIRequiredDeviceCapabilities 41 | 42 | armv7 43 | 44 | UISupportedInterfaceOrientations 45 | 46 | UIInterfaceOrientationPortrait 47 | UIInterfaceOrientationLandscapeLeft 48 | UIInterfaceOrientationLandscapeRight 49 | 50 | UIViewControllerBasedStatusBarAppearance 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /src/ModLive2DPreview.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {WebView} from 'react-native-webview' 3 | import {connect} from 'react-redux' 4 | import {Dimensions} from 'react-native' 5 | 6 | function ModLive2DPreview({hash, code, variant}) { 7 | const screenWidth = Math.round(Dimensions.get('window').width), 8 | INJECTEDJAVASCRIPT = ` 9 | const meta = document.createElement('meta'); meta.setAttribute('content', 'width=device-width, initial-scale=1.3, maximum-scale=10, user-scalable=10'); meta.setAttribute('name', 'viewport'); document.getElementsByTagName('head')[0].appendChild(meta); 10 | document.getElementById('canvas').style.marginLeft = '-${(screenWidth * 1.3 - screenWidth) / 2}px' 11 | ` 12 | 13 | return ( 14 | <> 15 | 24 | {/* */} 32 | 33 | ) 34 | } 35 | 36 | export default connect( 37 | ({mods}, {hash}) => { 38 | return mods[hash] 39 | ? { 40 | code: mods[hash].code, 41 | variant: mods[hash].variant 42 | } 43 | : {} 44 | } 45 | )(ModLive2DPreview) -------------------------------------------------------------------------------- /ios/DcModManagerTests/DcModManagerTests.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | #import 5 | #import 6 | 7 | #define TIMEOUT_SECONDS 600 8 | #define TEXT_TO_LOOK_FOR @"Welcome to React" 9 | 10 | @interface DcModManagerTests : XCTestCase 11 | 12 | @end 13 | 14 | @implementation DcModManagerTests 15 | 16 | - (BOOL)findSubviewInView:(UIView *)view matching:(BOOL(^)(UIView *view))test 17 | { 18 | if (test(view)) { 19 | return YES; 20 | } 21 | for (UIView *subview in [view subviews]) { 22 | if ([self findSubviewInView:subview matching:test]) { 23 | return YES; 24 | } 25 | } 26 | return NO; 27 | } 28 | 29 | - (void)testRendersWelcomeScreen 30 | { 31 | UIViewController *vc = [[[RCTSharedApplication() delegate] window] rootViewController]; 32 | NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS]; 33 | BOOL foundElement = NO; 34 | 35 | __block NSString *redboxError = nil; 36 | #ifdef DEBUG 37 | RCTSetLogFunction(^(RCTLogLevel level, RCTLogSource source, NSString *fileName, NSNumber *lineNumber, NSString *message) { 38 | if (level >= RCTLogLevelError) { 39 | redboxError = message; 40 | } 41 | }); 42 | #endif 43 | 44 | while ([date timeIntervalSinceNow] > 0 && !foundElement && !redboxError) { 45 | [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; 46 | [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; 47 | 48 | foundElement = [self findSubviewInView:vc.view matching:^BOOL(UIView *view) { 49 | if ([view.accessibilityLabel isEqualToString:TEXT_TO_LOOK_FOR]) { 50 | return YES; 51 | } 52 | return NO; 53 | }]; 54 | } 55 | 56 | #ifdef DEBUG 57 | RCTSetLogFunction(RCTDefaultLogFunction); 58 | #endif 59 | 60 | XCTAssertNil(redboxError, @"RedBox error: %@", redboxError); 61 | XCTAssertTrue(foundElement, @"Couldn't find element with text '%@' in %d seconds", TEXT_TO_LOOK_FOR, TIMEOUT_SECONDS); 62 | } 63 | 64 | 65 | @end 66 | -------------------------------------------------------------------------------- /src/ModPreview.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {connect} from 'react-redux' 3 | import {TouchableHighlight, View} from 'react-native' 4 | import ScaledImage from './ScaledImage' 5 | import ModderCreditLink from './ModderCreditLink' 6 | import {pushView} from './actions/view' 7 | import {Card, Paragraph, useTheme, Text} from 'react-native-paper' 8 | 9 | function ModPreview({mod: {code, variant, modder}, hash, pushView, character, view}) { 10 | const {colors} = useTheme() 11 | return (code && character) 12 | ? ( 13 | pushView('mod', {hash})} key={hash} style={{marginBottom: 20}}> 14 | 15 | 16 | 17 | 21 | 22 | 23 | {/* {modder && 24 | 28 | { 29 | if(view.data.modder != modder) pushView('modder', {modder}) 30 | }}> 31 | 32 | by {modder} 33 | 34 | 35 | 36 | } */} 37 | 38 | 39 | ) 40 | : null 41 | } 42 | 43 | export default connect( 44 | ({mods, characters, view}, {hash}) => ({ 45 | mod: mods[hash] || {}, 46 | character: mods[hash] && characters[mods[hash].code], 47 | hash, 48 | view 49 | }), 50 | ({pushView}) 51 | )(ModPreview) -------------------------------------------------------------------------------- /src/Lists.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {connect} from 'react-redux' 3 | import {View} from 'react-native' 4 | import {Headline, Button, Paragraph} from 'react-native-paper' 5 | import {pushView} from './actions/view' 6 | 7 | const Lists = ({pushView, lists, community}) => { 8 | return ( 9 | 10 | 11 | Personal Mod Lists 12 | 13 | 14 | {lists.length == 0 15 | ? You have not created any lists yet 16 | : lists.map(list => 17 | 20 | ) 21 | } 22 | 23 | 26 | 27 | 28 | Community Mod Lists 29 | 30 | 31 | The following lists are currated by the community. If you want to suggest a change or submit a list, use the Feedback option in the menu on the top right or ping Phasma on Discord. 32 | 33 | 34 | {Object.keys(community).length == 0 35 | ? Loading community lists ... 36 | : Object.keys(community).map(listName => { 37 | const list = community[listName] 38 | return ( 39 | 42 | ) 43 | }) 44 | } 45 | 46 | 47 | 48 | ) 49 | } 50 | 51 | export default connect( 52 | state => ({ 53 | lists: state.lists.lists, 54 | community: state.lists.community 55 | }), 56 | ({pushView}) 57 | )(Lists) -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | ; We fork some components by platform 3 | .*/*[.]android.js 4 | 5 | ; Ignore "BUCK" generated dirs 6 | /\.buckd/ 7 | 8 | ; Ignore polyfills 9 | node_modules/react-native/Libraries/polyfills/.* 10 | 11 | ; These should not be required directly 12 | ; require from fbjs/lib instead: require('fbjs/lib/warning') 13 | node_modules/warning/.* 14 | 15 | ; Flow doesn't support platforms 16 | .*/Libraries/Utilities/LoadingView.js 17 | 18 | [untyped] 19 | .*/node_modules/@react-native-community/cli/.*/.* 20 | 21 | [include] 22 | 23 | [libs] 24 | node_modules/react-native/interface.js 25 | node_modules/react-native/flow/ 26 | 27 | [options] 28 | emoji=true 29 | 30 | esproposal.optional_chaining=enable 31 | esproposal.nullish_coalescing=enable 32 | 33 | module.file_ext=.js 34 | module.file_ext=.json 35 | module.file_ext=.ios.js 36 | 37 | munge_underscores=true 38 | 39 | module.name_mapper='^react-native/\(.*\)$' -> '/node_modules/react-native/\1' 40 | module.name_mapper='^@?[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> '/node_modules/react-native/Libraries/Image/RelativeImageStub' 41 | 42 | suppress_type=$FlowIssue 43 | suppress_type=$FlowFixMe 44 | suppress_type=$FlowFixMeProps 45 | suppress_type=$FlowFixMeState 46 | 47 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(\\)? *\\(site=[a-z,_]*react_native\\(_ios\\)?_\\(oss\\|fb\\)[a-z,_]*\\)?)\\) 48 | suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(\\)? *\\(site=[a-z,_]*react_native\\(_ios\\)?_\\(oss\\|fb\\)[a-z,_]*\\)?)\\)?:? #[0-9]+ 49 | suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError 50 | 51 | [lints] 52 | sketchy-null-number=warn 53 | sketchy-null-mixed=warn 54 | sketchy-number=warn 55 | untyped-type-import=warn 56 | nonstrict-import=warn 57 | deprecated-type=warn 58 | unsafe-getters-setters=warn 59 | inexact-spread=warn 60 | unnecessary-invariant=warn 61 | signature-verification-failure=warn 62 | deprecated-utility=error 63 | 64 | [strict] 65 | deprecated-type 66 | nonstrict-import 67 | sketchy-null 68 | unclear-type 69 | unsafe-getters-setters 70 | untyped-import 71 | untyped-type-import 72 | 73 | [version] 74 | ^0.113.0 75 | -------------------------------------------------------------------------------- /ios/DcModManager/AppDelegate.m: -------------------------------------------------------------------------------- 1 | #import "AppDelegate.h" 2 | 3 | #import 4 | #import 5 | #import 6 | 7 | #if DEBUG 8 | #import 9 | #import 10 | #import 11 | #import 12 | #import 13 | #import 14 | 15 | static void InitializeFlipper(UIApplication *application) { 16 | FlipperClient *client = [FlipperClient sharedClient]; 17 | SKDescriptorMapper *layoutDescriptorMapper = [[SKDescriptorMapper alloc] initWithDefaults]; 18 | [client addPlugin:[[FlipperKitLayoutPlugin alloc] initWithRootNode:application withDescriptorMapper:layoutDescriptorMapper]]; 19 | [client addPlugin:[[FKUserDefaultsPlugin alloc] initWithSuiteName:nil]]; 20 | [client addPlugin:[FlipperKitReactPlugin new]]; 21 | [client addPlugin:[[FlipperKitNetworkPlugin alloc] initWithNetworkAdapter:[SKIOSNetworkAdapter new]]]; 22 | [client start]; 23 | } 24 | #endif 25 | 26 | @implementation AppDelegate 27 | 28 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 29 | { 30 | #if DEBUG 31 | InitializeFlipper(application); 32 | #endif 33 | 34 | RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions]; 35 | RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge 36 | moduleName:@"DcModManager" 37 | initialProperties:nil]; 38 | 39 | rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1]; 40 | 41 | self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; 42 | UIViewController *rootViewController = [UIViewController new]; 43 | rootViewController.view = rootView; 44 | self.window.rootViewController = rootViewController; 45 | [self.window makeKeyAndVisible]; 46 | return YES; 47 | } 48 | 49 | - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge 50 | { 51 | #if DEBUG 52 | return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil]; 53 | #else 54 | return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; 55 | #endif 56 | } 57 | 58 | @end 59 | -------------------------------------------------------------------------------- /src/actions/config.js: -------------------------------------------------------------------------------- 1 | export const CONFIG_SET = 'CONFIG_SET' 2 | import {setLoading} from './loading' 3 | import {loadInstalled} from './installed' 4 | import {setView} from './view' 5 | import RNFS from 'react-native-fs' 6 | import {getSettingsPath, getDestinyChildPath} from '../lib/paths' 7 | 8 | export const loadConfig = () => 9 | (dispatch) => { 10 | const installedRegions = [], 11 | regionPromises = []; 12 | ['global', 'kr', 'jp'].forEach(region => { 13 | regionPromises.push(RNFS.exists(getDestinyChildPath(region)).then(exists => { 14 | if(exists) installedRegions.push(region) 15 | })) 16 | }) 17 | Promise.all(regionPromises).then(() => { 18 | RNFS.exists(getSettingsPath()) 19 | .then(exists => { 20 | if(!exists) { 21 | saveConfig({installedRegions}).then(() => dispatch(loadConfig())) 22 | } 23 | else { 24 | RNFS.readFile(getSettingsPath()) 25 | .then(config => { 26 | try { 27 | config = JSON.parse(config) 28 | } 29 | catch(e) { 30 | config = {} 31 | } 32 | config.installedRegions = installedRegions 33 | if(installedRegions.indexOf(config.region) < 0) config.region = installedRegions[0] 34 | config.defaultView = config.defaultView || 'mods' 35 | dispatch(setView(config.defaultView)) 36 | config.defaultModsSortOrder = config.defaultModsSortOrder || 'recently added' 37 | config.defaultCharacterSortOrder = config.defaultCharacterSortOrder || 'code' 38 | config.defaultCharacterShow = config.defaultCharacterShow || 'all' 39 | dispatch(setConfig(config)) 40 | dispatch(loadInstalled()) 41 | }) 42 | .catch((err) => { 43 | console.log(err.message, err.code); 44 | }) 45 | } 46 | }) 47 | 48 | }) 49 | } 50 | 51 | const saveConfig = config => RNFS.writeFile(getSettingsPath(), JSON.stringify(config, null, 2)) 52 | 53 | export const setConfig = config => 54 | (dispatch, getState) => { 55 | config = {...getState().config, ...config} 56 | dispatch(setLoading(true, {title: 'Saving settings'})) 57 | dispatch({type: CONFIG_SET, config}) 58 | saveConfig(config).then(() => dispatch(setLoading(false))) 59 | } -------------------------------------------------------------------------------- /ios/DcModManager/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | DcModManager 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | LSRequiresIPhoneOS 26 | 27 | NSAppTransportSecurity 28 | 29 | NSAllowsArbitraryLoads 30 | 31 | NSExceptionDomains 32 | 33 | localhost 34 | 35 | NSExceptionAllowsInsecureHTTPLoads 36 | 37 | 38 | 39 | 40 | NSLocationWhenInUseUsageDescription 41 | 42 | UILaunchStoryboardName 43 | LaunchScreen 44 | UIRequiredDeviceCapabilities 45 | 46 | armv7 47 | 48 | UISupportedInterfaceOrientations 49 | 50 | UIInterfaceOrientationPortrait 51 | UIInterfaceOrientationLandscapeLeft 52 | UIInterfaceOrientationLandscapeRight 53 | 54 | UIViewControllerBasedStatusBarAppearance 55 | 56 | UIAppFonts 57 | 58 | AntDesign.ttf 59 | Entypo.ttf 60 | EvilIcons.ttf 61 | Feather.ttf 62 | FontAwesome.ttf 63 | FontAwesome5_Brands.ttf 64 | FontAwesome5_Regular.ttf 65 | FontAwesome5_Solid.ttf 66 | Fontisto.ttf 67 | Foundation.ttf 68 | Ionicons.ttf 69 | MaterialCommunityIcons.ttf 70 | MaterialIcons.ttf 71 | Octicons.ttf 72 | SimpleLineIcons.ttf 73 | Zocial.ttf 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /src/Tools.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {View} from 'react-native' 3 | import {Text, Headline, Card, Button} from 'react-native-paper' 4 | import {connect} from 'react-redux' 5 | import {setLoading} from './actions/loading' 6 | import {writeModelInfo, readModelInfo} from './lib/model-info' 7 | import {pushView} from './actions/view' 8 | import {detectInstalled} from './lib/installed' 9 | 10 | const Tools = ({setLoading, installed, modelInfo, mods, pushView}) => { 11 | const reApplyModelInfo = () => { 12 | setLoading(true, {title: 'Re-applying mod positions', message: `Processing ${Object.keys(installed).length} installed mods ...`}) 13 | readModelInfo().then(localModelInfo => { 14 | Object.keys(installed).forEach(target => { 15 | const {code, variant} = mods[installed[target].hash] || {} 16 | if(code && variant && modelInfo[code + '_' + variant]) { 17 | localModelInfo[target] = modelInfo[code + '_' + variant] 18 | } 19 | }) 20 | writeModelInfo(localModelInfo).then(() => setLoading(false)) 21 | }) 22 | } 23 | return ( 24 | 25 | Tools 26 | 27 | 28 | 29 | 32 | 33 | Attempts to detect all mods currently installed. May take a few minutes! 34 | 35 | 36 | 37 | 38 | 39 | 40 | 43 | 44 | This will re-apply all edits to model_info.json that have been made while installing mods. This is most useful after an update where model_info.json gets restored to the game's default and character positions and scal can get messed up for swaps. 45 | 46 | 49 | 50 | Edit position or scale details. 51 | 52 | 53 | 54 | 55 | ) 56 | } 57 | 58 | export default connect( 59 | ({installed, modelInfo, mods}) => ({installed, modelInfo, mods}), 60 | {setLoading, pushView} 61 | )(Tools) -------------------------------------------------------------------------------- /src/Character.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {connect} from 'react-redux' 3 | import {View, TouchableHighlight} from 'react-native' 4 | import {Subheading, useTheme, Headline, Text} from 'react-native-paper' 5 | import {pushView} from './actions/view' 6 | import ModPreview from './ModPreview' 7 | import InstalledPreview from './InstalledPreview' 8 | 9 | function Character({character, pushView, code, installed}) { 10 | const {colors} = useTheme(), 11 | installedVariants = character && Object.keys(character.variants) 12 | .reduce((acc, variant) => { 13 | const target = character.code + '_' + variant 14 | if(installed[target]) { 15 | acc[target] = installed[target] 16 | } 17 | return acc 18 | }, {}) 19 | return character 20 | ? ( 21 | 22 | 23 | pushView('characters')}> 24 | Characters 25 | 26 | > 27 | {character.name || character.code} 28 | 29 | 30 | Installed {character.name || character.code} Mods 31 | 32 | {Object.keys(installedVariants).length 33 | ? Object.keys(installedVariants).sort().map(target => 34 | 35 | ) 36 | : It doesn't look like you've installed any mods for {character.name || character.code} yet. 37 | } 38 | {Object.keys(character.variants).sort().map(variant => ( 39 | 40 | 41 | {} // pushView('variant', {code, variant}) 42 | }> 43 | 44 | {/* {character.variants[variant].title} {character.name} ({character.code}_{variant}) */} 45 | {character.code}_{variant} {character.variants[variant].title} {character.name || '?'} Mods 46 | 47 | 48 | {character.variants[variant].mods.map(hash => ( 49 | 50 | ))} 51 | 52 | ))} 53 | 54 | ) 55 | : null 56 | } 57 | 58 | export default connect( 59 | ({characters, installed, view}) => ({ 60 | character: characters[view.data.code], 61 | code: view.data.code, 62 | installed 63 | }), 64 | {pushView} 65 | )(Character) -------------------------------------------------------------------------------- /android/app/src/main/java/com/arsylk/mammonsmite/DestinyChild/DCModelInfo.java: -------------------------------------------------------------------------------- 1 | package com.arsylk.mammonsmite.DestinyChild; 2 | 3 | // import com.arsylk.mammonsmite.utils.Define; 4 | import com.arsylk.mammonsmite.utils.Utils; 5 | import org.json.JSONObject; 6 | import com.dcmodmanager.DCTools; 7 | 8 | public class DCModelInfo { 9 | private JSONObject namesJson, infoJson; 10 | 11 | private static DCModelInfo instance = null; 12 | public static DCModelInfo getInstance() { 13 | return getInstance(false); 14 | } 15 | public static DCModelInfo getInstance(boolean force) { 16 | if(instance == null || force) { 17 | instance = new DCModelInfo(); 18 | } 19 | 20 | return instance; 21 | } 22 | 23 | private DCModelInfo() { 24 | load(); 25 | } 26 | 27 | private void load() { 28 | //load child names 29 | // JSONObject json = Utils.fileToJson(Define.ASSET_EXTRACTED_CHILD_NAMES); 30 | // if(json != null) { 31 | // this.namesJson = json; 32 | // }else { 33 | // this.namesJson = null; 34 | // } 35 | 36 | //load model info 37 | JSONObject json = Utils.fileToJson(DCTools.getDCModelInfoPath()); 38 | if(json != null) { 39 | this.infoJson = json; 40 | }else { 41 | this.infoJson = null; 42 | } 43 | } 44 | 45 | //getters 46 | public JSONObject getNamesJson() { 47 | return namesJson; 48 | } 49 | 50 | public JSONObject getInfoJson() { 51 | return infoJson; 52 | } 53 | 54 | public String getModelName(String model_id) { 55 | try{ 56 | JSONObject modelJson = namesJson.getJSONObject(model_id); 57 | return modelJson.getString("name"); 58 | }catch(Exception e) { 59 | return null; 60 | } 61 | } 62 | 63 | public String getModelTitle(String model_id, String model_flag) { 64 | try{ 65 | JSONObject modelJson = namesJson.getJSONObject(model_id); 66 | JSONObject modelVariants = modelJson.getJSONObject("variants"); 67 | JSONObject modelFlag = modelVariants.getJSONObject(model_flag); 68 | return modelFlag.getString("title"); 69 | }catch(Exception e) { 70 | return null; 71 | } 72 | } 73 | 74 | public String getModelFull(String modelIdx) { 75 | try { 76 | String[] parts = modelIdx.split("_"); 77 | return getModelTitle(parts[0], parts[1]) + " " + getModelName(parts[0]); 78 | }catch(Exception e) { 79 | return null; 80 | } 81 | } 82 | 83 | public JSONObject getModelInfo(String modelIdx) { 84 | try{ 85 | return infoJson.getJSONObject(modelIdx); 86 | }catch(Exception e) { 87 | return null; 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/InstalledMods.js: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react' 2 | import {View} from 'react-native' 3 | import {Headline, Text, Button, TextInput, DataTable} from 'react-native-paper' 4 | import {connect} from 'react-redux' 5 | import InstalledPreview from './InstalledPreview' 6 | import {pushView} from './actions/view' 7 | import { detectInstalled } from './lib/installed' 8 | 9 | const InstalledMods = ({installed, characters, pushView, page = 0}) => { 10 | const [filter, setFilter] = useState(''), 11 | modKeys = Object.keys(installed).sort(), 12 | itemsPerPage = 10, 13 | filteredModKeys = filter.replace(/\s/g, '') 14 | ? modKeys.filter(key => { 15 | const [code, variant] = key.split('_') 16 | return (key + characters[code].variants[variant].title + ' ' + characters[code].name).toLowerCase().match(filter.toLowerCase()) 17 | }) 18 | : modKeys, 19 | from = page * itemsPerPage, 20 | to = Math.min((page + 1) * itemsPerPage, filteredModKeys.length), 21 | numPages = Math.ceil(filteredModKeys.length / itemsPerPage) 22 | return ( 23 | 24 | Installed Mods 25 | 28 | 33 | pushView('installed', {page})} 37 | label={`${from + 1}-${to} of ${filteredModKeys.length}`} 38 | /> 39 | {modKeys.length 40 | ? filteredModKeys.length 41 | ? filteredModKeys.slice(from, to).map(target => 42 | 43 | ) 44 | : No installed mods match your filter 45 | : <> 46 | It doesn't look like you have installed any mods with this app yet (or since install tracking was implemented). Installed mods should start showing up here once you install some. 47 | 50 | 51 | } 52 | pushView('installed', {page})} 56 | label={`${from + 1}-${to} of ${filteredModKeys.length}`} 57 | /> 58 | 59 | ) 60 | } 61 | 62 | export default connect( 63 | ({installed, characters, view}) => ({ 64 | installed, 65 | characters, 66 | page: view.data.page 67 | }), 68 | {pushView} 69 | )(InstalledMods) -------------------------------------------------------------------------------- /android/app/src/main/java/com/dcmodmanager/MainApplication.java: -------------------------------------------------------------------------------- 1 | package com.dcmodmanager; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | import com.facebook.react.PackageList; 6 | import com.facebook.react.ReactApplication; 7 | import com.nodece.apkinstallern.RNApkInstallerNPackage; 8 | import com.RNFetchBlob.RNFetchBlobPackage; 9 | import com.RNFetchBlob.RNFetchBlobPackage; 10 | import com.oblador.vectoricons.VectorIconsPackage; 11 | import com.facebook.react.ReactInstanceManager; 12 | import com.facebook.react.ReactNativeHost; 13 | import com.facebook.react.ReactPackage; 14 | import com.facebook.soloader.SoLoader; 15 | import java.lang.reflect.InvocationTargetException; 16 | import java.util.List; 17 | 18 | public class MainApplication extends Application implements ReactApplication { 19 | 20 | private final ReactNativeHost mReactNativeHost = 21 | new ReactNativeHost(this) { 22 | @Override 23 | public boolean getUseDeveloperSupport() { 24 | return BuildConfig.DEBUG; 25 | } 26 | 27 | @Override 28 | protected List getPackages() { 29 | @SuppressWarnings("UnnecessaryLocalVariable") 30 | List packages = new PackageList(this).getPackages(); 31 | // Packages that cannot be autolinked yet can be added manually here, for example: 32 | // packages.add(new MyReactNativePackage()); 33 | packages.add(new CustomPackage()); 34 | return packages; 35 | } 36 | 37 | @Override 38 | protected String getJSMainModuleName() { 39 | return "index"; 40 | } 41 | }; 42 | 43 | @Override 44 | public ReactNativeHost getReactNativeHost() { 45 | return mReactNativeHost; 46 | } 47 | 48 | @Override 49 | public void onCreate() { 50 | super.onCreate(); 51 | SoLoader.init(this, /* native exopackage */ false); 52 | initializeFlipper(this, getReactNativeHost().getReactInstanceManager()); 53 | } 54 | 55 | /** 56 | * Loads Flipper in React Native templates. Call this in the onCreate method with something like 57 | * initializeFlipper(this, getReactNativeHost().getReactInstanceManager()); 58 | * 59 | * @param context 60 | * @param reactInstanceManager 61 | */ 62 | private static void initializeFlipper( 63 | Context context, ReactInstanceManager reactInstanceManager) { 64 | if (BuildConfig.DEBUG) { 65 | try { 66 | /* 67 | We use reflection here to pick up the class that initializes Flipper, 68 | since Flipper library is not available in release mode 69 | */ 70 | Class aClass = Class.forName("com.dcmodmanager.ReactNativeFlipper"); 71 | aClass 72 | .getMethod("initializeFlipper", Context.class, ReactInstanceManager.class) 73 | .invoke(null, context, reactInstanceManager); 74 | } catch (ClassNotFoundException e) { 75 | e.printStackTrace(); 76 | } catch (NoSuchMethodException e) { 77 | e.printStackTrace(); 78 | } catch (IllegalAccessException e) { 79 | e.printStackTrace(); 80 | } catch (InvocationTargetException e) { 81 | e.printStackTrace(); 82 | } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Drawer.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {Text, Surface, Drawer, Subheading, TouchableRipple} from 'react-native-paper' 3 | import {Linking, Divider, View, Dimensions, ScrollView, TouchableOpacity} from 'react-native' 4 | import {connect} from 'react-redux' 5 | import {pushView} from './actions/view' 6 | import version from './version.json' 7 | 8 | const DrawerNavigation = ({onClose, view, pushView}) => { 9 | const navigate = (viewName, viewData) => { 10 | pushView(viewName, viewData) 11 | onClose() 12 | } 13 | return ( 14 | <> 15 | 26 | 33 | 34 | 35 | navigate('mods')} /> 41 | navigate('modders')} /> 46 | navigate('characters')} /> 51 | navigate('installed')} /> 56 | navigate('lists')} /> 61 | navigate('tools')} /> 66 | navigate('settings')} /> 71 | 72 | { 73 | Linking.openURL('https://github.com/PhasmaExMachina/dc-mod-manager/releases') 74 | }}> 75 | version {version} 76 | 77 | 78 | 79 | 80 | 81 | ) 82 | } 83 | 84 | export default connect( 85 | ({view}) => ({view}), 86 | {pushView} 87 | )(DrawerNavigation) -------------------------------------------------------------------------------- /src/Variant.js: -------------------------------------------------------------------------------- 1 | import React, {useState, useEffect} from 'react' 2 | import {View, TouchableHighlight} from 'react-native' 3 | import {Headline, Subheading, useTheme, Paragraph, Card, Button} from 'react-native-paper' 4 | import {connect} from 'react-redux' 5 | import {pushView} from './actions/view' 6 | import Slider from '@react-native-community/slider' 7 | import {readModelInfo, writeModelInfo} from './lib/model-info' 8 | import {setLoading} from './actions/loading' 9 | 10 | const Variant = ({character, variant, code, pushView, setLoading}) => { 11 | const [homeScale, setHomeScale] = useState(), 12 | [modelInfo, setModelInfo] = useState(false), 13 | {colors} = useTheme() 14 | useEffect(() => { 15 | setLoading(true, {title: 'Loading model_info.json', message: 'Reading latest model_info.json data from your device.'}) 16 | readModelInfo().then(m => { 17 | setModelInfo(m) 18 | setHomeScale(m[code + '_' + variant].home.scale) 19 | }) 20 | } 21 | , []) 22 | if(modelInfo) setLoading(false) 23 | else return null 24 | const charModelInfo = modelInfo[code + '_' + variant] 25 | return ( 26 | 27 | 28 | pushView('characters')}> 29 | Characters 30 | 31 | > 32 | pushView('character', {code})}> 33 | {character.name || character.code} 34 | 35 | > 36 | {code}_{variant} 37 | 38 | 39 | {character.variants[variant].title} {character.name || '?'} ({code}_{variant}) 40 | 41 | 42 | 43 | 44 | 54 | {/* {JSON.stringify(charModelInfo.home, null, 2)} */} 55 | 56 | 57 | 63 | {JSON.stringify(charModelInfo, null, 2)} 64 | 65 | ) 66 | } 67 | 68 | export default connect( 69 | ({modelInfo, characters, view: {data: {code, variant}}}) => ({ 70 | character: characters[code] || {}, 71 | variant, 72 | code, 73 | modelInfo, 74 | setLoading 75 | }), 76 | {pushView, setLoading} 77 | )(Variant) -------------------------------------------------------------------------------- /android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 33 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 34 | 35 | @rem Find java.exe 36 | if defined JAVA_HOME goto findJavaFromJavaHome 37 | 38 | set JAVA_EXE=java.exe 39 | %JAVA_EXE% -version >NUL 2>&1 40 | if "%ERRORLEVEL%" == "0" goto init 41 | 42 | echo. 43 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 44 | echo. 45 | echo Please set the JAVA_HOME variable in your environment to match the 46 | echo location of your Java installation. 47 | 48 | goto fail 49 | 50 | :findJavaFromJavaHome 51 | set JAVA_HOME=%JAVA_HOME:"=% 52 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 53 | 54 | if exist "%JAVA_EXE%" goto init 55 | 56 | echo. 57 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 58 | echo. 59 | echo Please set the JAVA_HOME variable in your environment to match the 60 | echo location of your Java installation. 61 | 62 | goto fail 63 | 64 | :init 65 | @rem Get command-line arguments, handling Windows variants 66 | 67 | if not "%OS%" == "Windows_NT" goto win9xME_args 68 | 69 | :win9xME_args 70 | @rem Slurp the command line arguments. 71 | set CMD_LINE_ARGS= 72 | set _SKIP=2 73 | 74 | :win9xME_args_slurp 75 | if "x%~1" == "x" goto execute 76 | 77 | set CMD_LINE_ARGS=%* 78 | 79 | :execute 80 | @rem Setup the command line 81 | 82 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 83 | 84 | @rem Execute Gradle 85 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 86 | 87 | :end 88 | @rem End local scope for the variables with windows NT shell 89 | if "%ERRORLEVEL%"=="0" goto mainEnd 90 | 91 | :fail 92 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 93 | rem the _cmd.exe /c_ return code! 94 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 95 | exit /b 1 96 | 97 | :mainEnd 98 | if "%OS%"=="Windows_NT" endlocal 99 | 100 | :omega 101 | -------------------------------------------------------------------------------- /src/actions/mods.js: -------------------------------------------------------------------------------- 1 | export const MODS_SET = 'MODS_SET' 2 | import RNFS from 'react-native-fs' 3 | import RNFetchBlob from 'rn-fetch-blob' 4 | import Toast from 'react-native-simple-toast' 5 | import swap from '../lib/swap' 6 | import {setLoading} from './loading' 7 | import {getCharactersPath} from '../lib/paths' 8 | import {writeInstalled} from '../lib/installed' 9 | import {loadInstalled} from '../actions/installed' 10 | 11 | export const fetchMods = () => 12 | (dispatch) => fetch('https://phasmaexmachina.github.io/destiny-child-mods-archive/data/mods.json') 13 | .then(response => response.json()) 14 | .then(data => dispatch(setMods(data))); 15 | 16 | export const setMods = mods => ({type: MODS_SET, mods}) 17 | 18 | const _install = ({hash, code, variant}, target, {characters}, dispatch, message = true) => { 19 | const source = code + '_' + variant 20 | target = target || source 21 | if(message) { 22 | dispatch(setLoading(true, {title: 'Installing mod', message: `Downloading ${source}.pck ...`})) 23 | } 24 | return RNFetchBlob.config({fileCache : true}).fetch('GET', `https://raw.githubusercontent.com/PhasmaExMachina/destiny-child-mods-archive/master/mods/${hash}/${code}_${variant}.pck`).then((res) => { 25 | const installedTo = [], 26 | attempts = [], 27 | complete = () => RNFS.unlink(res.path()) 28 | if(message) { 29 | dispatch(setLoading(true, {title: 'Installing mod', message: `Installing ${source} into ${target} ...`})) 30 | } 31 | return swap( 32 | res.path(), 33 | getCharactersPath() + `${target}.pck`, 34 | source, 35 | target, 36 | hash 37 | ).then(complete) 38 | }) 39 | } 40 | 41 | export const install = ({hash, code, variant}, target, message = true) => 42 | (dispatch, getState) => { 43 | _install({hash, code, variant}, target, getState(), dispatch, message) 44 | .then(() => { 45 | Toast.show(`Installed to ${target}`) 46 | const {installed} = getState() 47 | installed[target || code + '_' + variant] = {hash} 48 | dispatch(setLoading(true, {title: 'Saving install information', message: 'Storing installed mod information for later.'})) 49 | writeInstalled(installed).then(() => dispatch(setLoading(false))) 50 | dispatch(loadInstalled()) 51 | }) 52 | } 53 | 54 | 55 | export const installList = list => 56 | (dispatch, getState) => { 57 | const targets = Object.keys(list.mods), 58 | {mods, characters, installed} = getState() 59 | console.log('installing list', list) 60 | let i = -1 61 | const processNextMod = () => { 62 | i++ 63 | if(i == targets.length) { 64 | writeInstalled(installed).then(() => { 65 | dispatch(loadInstalled()) 66 | dispatch(setLoading(false)) 67 | }) 68 | return 69 | } 70 | else { 71 | dispatch(setLoading(true, { 72 | title: 'Installing ' + list.name, 73 | message: 'Installing ' + targets[i] + ' ...', 74 | progress: i, 75 | total: targets.length 76 | })) 77 | const target = targets[i], 78 | hash = list.mods[target].hash, 79 | mod = Object.assign({hash}, mods[hash]) 80 | _install(mod, target, {characters}, dispatch, false).then(() => { 81 | installed[target] = {hash} 82 | processNextMod() 83 | }) 84 | } 85 | } 86 | processNextMod() 87 | } -------------------------------------------------------------------------------- /src/ModderCreditLink.js: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react' 2 | import {connect} from 'react-redux' 3 | import {TouchableHighlight, View, Linking} from 'react-native' 4 | import {Portal, Dialog, Button, Paragraph, Text, useTheme} from 'react-native-paper' 5 | import {pushView} from './actions/view' 6 | 7 | const ModderCreditLink = ({view, mod, pushView, characters, hash}) => { 8 | const {code, variant, modder, usingAssetsBy} = mod, 9 | {colors} = useTheme(), 10 | {name, variants} = characters[mod.code], 11 | [infoDialogIsOpen, setInfoDialogIsOpen] = useState(false), 12 | modderCreditTicketTemplate = ` 13 | Use this form to sumbmit modder author/creator information for a given mod so they get credit for their work. I'll update the information in the archive as soon as I can and will close this ticket when it's done. ~Phasma 14 | 15 | Modder: 16 | 17 | If this mod uses assets by any other modders please list them here: 18 | 19 | Mod link (do not change this): 20 | https://phasmaexmachina.github.io/destiny-child-mods-archive/live2d-viewer.html?model=${code}_${variant}&modHash=${hash}&background=%23111 21 | 22 | Mod hash (do not change this): ${hash}` 23 | 24 | return variants[mod.variant].mods[0] != hash && ( 25 | 30 | { 31 | if(!modder) setInfoDialogIsOpen(true) 32 | else if(view.data.modder != modder) pushView('modder', {modder}) 33 | }}> 34 | 35 | by {modder || '?'} 36 | 37 | 38 | {usingAssetsBy && { 39 | if(view.data.modder != usingAssetsBy) pushView('modder', {modder: usingAssetsBy}) 40 | }}> 41 | 42 | {' '}using assets by {usingAssetsBy} 43 | 44 | 45 | } 46 | 47 | setInfoDialogIsOpen(false)}> 48 | Submit a modder? 49 | 50 | We don't know who made this mod. Would you like to dubmit a modder? You will need a GitHub account. 51 | 52 | 53 | 54 | 60 | 61 | 62 | 63 | 64 | ) 65 | } 66 | 67 | export default connect( 68 | ({mods, view, characters}, {hash}) => { 69 | const mod = mods[hash] || {} 70 | return { 71 | mod, 72 | hash, 73 | mods, 74 | view, 75 | characters 76 | } 77 | }, 78 | ({pushView}) 79 | )(ModderCreditLink) -------------------------------------------------------------------------------- /android/app/src/debug/java/com/dcmodmanager/ReactNativeFlipper.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | *

    This source code is licensed under the MIT license found in the LICENSE file in the root 5 | * directory of this source tree. 6 | */ 7 | package com.dcmodmanager; 8 | 9 | import android.content.Context; 10 | import com.facebook.flipper.android.AndroidFlipperClient; 11 | import com.facebook.flipper.android.utils.FlipperUtils; 12 | import com.facebook.flipper.core.FlipperClient; 13 | import com.facebook.flipper.plugins.crashreporter.CrashReporterPlugin; 14 | import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin; 15 | import com.facebook.flipper.plugins.fresco.FrescoFlipperPlugin; 16 | import com.facebook.flipper.plugins.inspector.DescriptorMapping; 17 | import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin; 18 | import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor; 19 | import com.facebook.flipper.plugins.network.NetworkFlipperPlugin; 20 | import com.facebook.flipper.plugins.react.ReactFlipperPlugin; 21 | import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin; 22 | import com.facebook.react.ReactInstanceManager; 23 | import com.facebook.react.bridge.ReactContext; 24 | import com.facebook.react.modules.network.NetworkingModule; 25 | import okhttp3.OkHttpClient; 26 | 27 | public class ReactNativeFlipper { 28 | public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) { 29 | if (FlipperUtils.shouldEnableFlipper(context)) { 30 | final FlipperClient client = AndroidFlipperClient.getInstance(context); 31 | 32 | client.addPlugin(new InspectorFlipperPlugin(context, DescriptorMapping.withDefaults())); 33 | client.addPlugin(new ReactFlipperPlugin()); 34 | client.addPlugin(new DatabasesFlipperPlugin(context)); 35 | client.addPlugin(new SharedPreferencesFlipperPlugin(context)); 36 | client.addPlugin(CrashReporterPlugin.getInstance()); 37 | 38 | NetworkFlipperPlugin networkFlipperPlugin = new NetworkFlipperPlugin(); 39 | NetworkingModule.setCustomClientBuilder( 40 | new NetworkingModule.CustomClientBuilder() { 41 | @Override 42 | public void apply(OkHttpClient.Builder builder) { 43 | builder.addNetworkInterceptor(new FlipperOkhttpInterceptor(networkFlipperPlugin)); 44 | } 45 | }); 46 | client.addPlugin(networkFlipperPlugin); 47 | client.start(); 48 | 49 | // Fresco Plugin needs to ensure that ImagePipelineFactory is initialized 50 | // Hence we run if after all native modules have been initialized 51 | ReactContext reactContext = reactInstanceManager.getCurrentReactContext(); 52 | if (reactContext == null) { 53 | reactInstanceManager.addReactInstanceEventListener( 54 | new ReactInstanceManager.ReactInstanceEventListener() { 55 | @Override 56 | public void onReactContextInitialized(ReactContext reactContext) { 57 | reactInstanceManager.removeReactInstanceEventListener(this); 58 | reactContext.runOnNativeModulesQueueThread( 59 | new Runnable() { 60 | @Override 61 | public void run() { 62 | client.addPlugin(new FrescoFlipperPlugin()); 63 | } 64 | }); 65 | } 66 | }); 67 | } else { 68 | client.addPlugin(new FrescoFlipperPlugin()); 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/EditList.js: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react' 2 | import {connect} from 'react-redux' 3 | import {View, TouchableHighlight, Alert} from 'react-native' 4 | import {Dialog, Paragraph, TextInput, useTheme, Headline, Subheading, Button, Portal} from 'react-native-paper' 5 | import {pushView} from './actions/view' 6 | import {saveList} from './actions/lists' 7 | import toFilename from './lib/to-filename' 8 | 9 | const EditList = ({pushView, saveList, list, lists}) => { 10 | const {colors} = useTheme(), 11 | [name, setName] = useState(list.name || ''), 12 | [description, setDescription] = useState(list.description || ''), 13 | [error, setError] = useState(false), 14 | saveListIfValid = () => { 15 | if(!name.replace(/\s/g, '')) { 16 | setError('List name cannot be empty') 17 | } 18 | else if(list.name != name && lists.map(l => toFilename(l.name)).indexOf(toFilename(name)) > -1) { 19 | setError('A list with that name already exists') 20 | } 21 | else saveList(Object.assign({}, list, {name, description}), list.name) 22 | } 23 | return ( 24 | 25 | 26 | pushView('lists')}> 27 | Mod Lists 28 | 29 | > 30 | {list.name && <> 31 | pushView('list', {list})}> 32 | {list.name} 33 | 34 | > 35 | } 36 | {list.name ? 'Edit' : 'New List'} 37 | 38 | 39 | {list.name ? 'Edit List' : 'Create a new list'} 40 | 41 | 42 | 48 | 49 | 50 | 54 | 55 | 56 | 62 | 65 | 66 | 67 |

    setError(false)}> 68 | Error 69 | 70 | {error} 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | ) 79 | } 80 | 81 | export default connect( 82 | state => ({ 83 | lists: state.lists.lists, 84 | list: state.view.data.list || {} 85 | }), 86 | ({pushView, saveList}) 87 | )(EditList) -------------------------------------------------------------------------------- /ios/DcModManager.xcodeproj/xcshareddata/xcschemes/DcModManager.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 55 | 61 | 62 | 63 | 64 | 70 | 72 | 78 | 79 | 80 | 81 | 83 | 84 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /ios/DcModManager.xcodeproj/xcshareddata/xcschemes/DcModManager-tvOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 55 | 61 | 62 | 63 | 64 | 70 | 72 | 78 | 79 | 80 | 81 | 83 | 84 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /ios/DcModManager/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 21 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/Mods.js: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react' 2 | import {connect} from 'react-redux' 3 | import { 4 | View, 5 | Button 6 | } from 'react-native' 7 | import {TextInput, Menu, DataTable, Headline} from 'react-native-paper' 8 | import {pushView} from './actions/view' 9 | import ModPreview from './ModPreview' 10 | import {scrollToTop} from './ScrollTop' 11 | 12 | function Mods({mods, characters, config, view, pushView}) { 13 | if(!config.defaultModsSortOrder) return null 14 | const [filter, setFilter] = useState(''), 15 | [sortMenuVisible, setSortMenuVisible] = useState(false), 16 | page = view.data.page || 0, 17 | sort = view.data.sort || config.defaultModsSortOrder, 18 | setPage = p => pushView('mods', {...view.data, page: p}) 19 | setSortAndClose = val => { 20 | pushView('mods', {...view.data, sort: val, page: 0}) 21 | setSortMenuVisible(false) 22 | }, 23 | itemsPerPage = 10, 24 | from = page * itemsPerPage, 25 | to = (page + 1) * itemsPerPage 26 | let filtered = Object.keys(mods).reduce((acc, hash) => { 27 | const {code, variant} = mods[hash] 28 | if(!filter || ((characters[code] ? (characters[code].name || '') : '') + code + '_' + variant).toLowerCase().match(filter.toLowerCase())) { 29 | acc.push({...mods[hash], hash}) 30 | } 31 | return acc 32 | }, []) 33 | if(sort == 'recently added') { 34 | filtered = filtered.sort((a, b) => a.created > b.created ? -1 : b.created > a.created ? 1 : 0) 35 | } 36 | if(sort == 'oldest') { 37 | filtered = filtered.sort((a, b) => a.created < b.created ? -1 : b.created < a.created ? 1 : 0) 38 | } 39 | if(sort == 'name') { 40 | filtered = filtered.sort((a, b) => { 41 | a = characters[mods[a.hash].code].name 42 | b = characters[mods[b.hash].code].name 43 | return a < b ? -1 : b < a ? 1 : 0 44 | }) 45 | } 46 | if(sort == 'code') { 47 | filtered = filtered.sort((a, b) => { 48 | a = mods[a.hash].code + '_' + mods[a.hash].variant 49 | b = mods[b.hash].code + '_' + mods[b.hash].variant 50 | return a < b ? -1 : b < a ? 1 : 0 51 | }) 52 | } 53 | return ( 54 | 55 | Mods 56 | 57 | setFilter(text)}/> 58 | 59 | 60 | setSortMenuVisible(false)} 64 | anchor={ 70 | 71 | setPage(page)} 75 | label={`${from + 1}-${to} of ${filtered.length}`} 76 | /> 77 | {filtered.slice(from, to).map(({hash}) => 78 | 79 | )} 80 | setPage(page)} 84 | label={`${from + 1}-${to} of ${filtered.length}`} 85 | /> 86 | 87 | ) 88 | } 89 | 90 | export default connect( 91 | ({mods, characters, config, view}) => ({mods, characters, config, view}), 92 | {pushView} 93 | )(Mods) -------------------------------------------------------------------------------- /src/actions/lists.js: -------------------------------------------------------------------------------- 1 | import RNFS from 'react-native-fs' 2 | import RNFetchBlob from 'rn-fetch-blob' 3 | import {getListsPath} from '../lib/paths' 4 | import toFilename from '../lib/to-filename' 5 | import {setLoading} from './loading' 6 | import {doInstall} from './mods' 7 | import {setView} from './view' 8 | 9 | export const LISTS_SET = 'LISTS_SET' 10 | export const LISTS_SET_ACTIVE = 'LISTS_SET_ACTIVE' 11 | export const LISTS_COMMUNITY_SET = 'LISTS_COMMUNITY_SET' 12 | 13 | export const loadLists = () => 14 | dispatch => { 15 | fetch('https://phasmaexmachina.github.io/destiny-child-mods-archive/data/lists.json') 16 | .then(response => response.json()) 17 | .then(listNames => { 18 | listNames.forEach(listName => { 19 | console.log(listName) 20 | return listName != 'index' && fetch(`https://phasmaexmachina.github.io/destiny-child-mods-archive/data/lists/${listName}.json`) 21 | .then(response => response.json()) 22 | .then(list => dispatch(setCommunityList(listName, list))) 23 | .catch(e => console.log('Error fetching list', listName)) 24 | }) 25 | }) 26 | .catch(e => console.log('Error fetching lists.json')) 27 | RNFS.exists(getListsPath()).then(exists => exists 28 | ? RNFS.readDir(getListsPath()) 29 | .then(files => { 30 | const listPromises = [], 31 | lists = [] 32 | files.forEach(file => { 33 | if(file.name.match(/\.json/)) { 34 | listPromises.push( 35 | RNFetchBlob.fs.readFile(getListsPath() + file.name, "utf8") 36 | .then(list => lists.push(JSON.parse(list))) 37 | .catch(e => console.log('error', e)) 38 | ) 39 | } 40 | }) 41 | Promise.all(listPromises).then(() => dispatch({ 42 | type: LISTS_SET, 43 | lists 44 | })) 45 | }) 46 | .catch(e => console.log('error', e)) 47 | : RNFS.mkdir(getListsPath()) 48 | .catch(e => console.log('error', e)) 49 | ) 50 | } 51 | 52 | export const saveList = (list, oldName, navigate = true) => 53 | (dispatch, getState) => { 54 | const newName = toFilename(list.name) 55 | list.mods = list.mods || {} 56 | const doSave = () => 57 | RNFetchBlob.fs.writeFile(getListsPath() + newName + '.json', JSON.stringify(list, null, 2), 'utf8') 58 | .then(() => { 59 | dispatch(setLoading(false)) 60 | dispatch(loadLists()) 61 | const {active} = getState().lists 62 | console.log('ACTIVE LIST', active) 63 | if(list == active) dispatch(setActiveList({...active})) 64 | if(navigate) dispatch(setView('list', {list})) 65 | }) 66 | dispatch(setLoading(true, {title: 'Saving list'})) 67 | console.log('savig', list.name, oldName) 68 | if(oldName) { 69 | RNFS.unlink(getListsPath() + toFilename(oldName) + '.json') 70 | .then(doSave) 71 | .catch(e => doSave()) 72 | } 73 | else doSave() 74 | } 75 | 76 | export const deleteList = (list, navigate = true) => 77 | dispatch => { 78 | RNFetchBlob.fs.unlink(getListsPath() + toFilename(list.name) + '.json') 79 | .then(() => { 80 | dispatch(loadLists()) 81 | dispatch(setView('lists')) 82 | }) 83 | .catch(e => console.log('error', e)) 84 | } 85 | 86 | export const setActiveList = list => ({ 87 | type: LISTS_SET_ACTIVE, 88 | list 89 | }) 90 | 91 | export const addModToList = (mod, target) => 92 | (dispatch, getState) => { 93 | const activeList = getState().lists.active 94 | activeList.mods[target] = mod 95 | dispatch(saveList(activeList, false, false)) 96 | } 97 | 98 | export const removeModFromList = (target, list) => 99 | (dispatch, getState) => { 100 | delete list.mods[target] 101 | dispatch(saveList(list, false, false)) 102 | // dispatch(setView('list', {list: {...list}})) 103 | } 104 | 105 | export const setCommunityList = (listName, list) => ({ 106 | type: LISTS_COMMUNITY_SET, 107 | list, 108 | listName 109 | }) -------------------------------------------------------------------------------- /src/Settings.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {Text, Title, Paragraph, Headline, Subheading, RadioButton, Card} from 'react-native-paper' 3 | import {connect} from 'react-redux' 4 | import {View} from 'react-native' 5 | import {setConfig} from './actions/config' 6 | 7 | const Settings = ({config, setConfig}) => { 8 | return ( 9 | 10 | Settings 11 | 12 | 13 | 14 | setConfig({defaultView: value})} value={config.defaultView}> 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | setConfig({defaultModsSortOrder: value})} value={config.defaultModsSortOrder}> 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | setConfig({defaultCharacterSortOrder: value})} value={config.defaultCharacterSortOrder}> 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | setConfig({defaultCharacterShow: value})} value={config.defaultCharacterShow}> 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | setConfig({region: value})} value={config.region}> 59 | 63 | 67 | 71 | 72 | 73 | 74 | 75 | ) 76 | } 77 | 78 | export default connect( 79 | ({config}) => ({config}), 80 | {setConfig} 81 | )(Settings) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DC Mod Manager app for Android 2 | 3 | DC Mod Manager is an app for Android that lets you browse mods/skins and install them with a single click. These are only cosmetic changes on your own device, and do not effect gameplay or other players in any way. 4 | 5 | ## Installation & Updates 6 | 7 | You can download the latest APK below or from the [releases page](https://github.com/PhasmaExMachina/dc-mod-manager/releases) by expanding the "Assets" dropdown under the latest release. The easiest way is to download it from your Android phone. See [What is an APK and how do you install one](https://www.androidpit.com/android-for-beginners-what-is-an-apk-file), or [ask Google](http://letmegooglethat.com/?q=how+to+install+apk) if you get stuck. 8 | 9 | **[Download latest APK v0.0.14](https://github.com/PhasmaExMachina/dc-mod-manager/releases/download/v0.0.14/dcmodmanager-v0.0.14.apk)** 10 | 11 | **Updating** is as easy as installing the newest APK on top of the old one. You should receive an in-app message when a new version is available. 12 | 13 | ## Features 14 | 15 | * Browse virtually every mod ever released 16 | * Live2D preview - based on work by [Arsylk](https://github.com/Arsylk) 17 | * Install mods with the push of a button 18 | * Create personal lists of mods installed to specific character variants 19 | * Community lists like fully uncensor - [nude all coming soon](https://github.com/PhasmaExMachina/dc-mod-manager/issues/16) 20 | * Install all mods in a lists with the push of a button 21 | * Swap mods into any variant or character - based on work by [Arsylk](https://github.com/Arsylk) 22 | * Automatically handles model_info.json positioning when swapping 23 | * One click tool to restore model_info.json positions after an update 24 | * Basic model_info.json editor to tweak character scale and position 25 | * Tool to automatically detect all installed mods 26 | * See modder credits and view mods by modder (work in progress) 27 | 28 | ## Known Issues 29 | 30 | A full list of known issues can be found [here](https://github.com/PhasmaExMachina/dc-mod-manager/issues/4), but these are the highlights: 31 | 32 | * [DC Mod Manager does not work on Bluestacks or Nox](https://github.com/PhasmaExMachina/dc-mod-manager/issues/4) 33 | 34 | ## Screenshots 35 | 36 | ![](https://raw.githubusercontent.com/PhasmaExMachina/dc-mod-manager/master/screenshots/screenshot-1.jpg) 37 | 38 | ![](https://raw.githubusercontent.com/PhasmaExMachina/dc-mod-manager/master/screenshots/screenshot-2.jpg) 39 | 40 | ![](https://raw.githubusercontent.com/PhasmaExMachina/dc-mod-manager/master/screenshots/screenshot-3.jpg) 41 | 42 | ![](https://raw.githubusercontent.com/PhasmaExMachina/dc-mod-manager/master/screenshots/screenshot-4.jpg) 43 | 44 | ![](https://raw.githubusercontent.com/PhasmaExMachina/dc-mod-manager/master/screenshots/screenshot-5.jpg) 45 | 46 | ![](https://raw.githubusercontent.com/PhasmaExMachina/dc-mod-manager/master/screenshots/screenshot-6.jpg) 47 | 48 | ## Credits 49 | 50 | This app is powered by the [Destiny Child Mods Archive](https://github.com/PhasmaExMachina/destiny-child-mods-archive) project. The mods in themselves are created by many different people. The goal was always to credit each modder on [the site](https://phasmaexmachina.github.io/destiny-child-mods-archive/) and in this app, but that's a _lot_ of manual work. Check out [this issue](https://github.com/PhasmaExMachina/destiny-child-mods-archive/issues/2) if you want to help out with the initiative. 51 | 52 | Many other coders and artists have done work over the years that has made this app possible including, but not limited to: 53 | 54 | * [Loki](https://en.wikipedia.org/wiki/Loki) - Modder and author of original mod archive and DC Mod Manager apps, now vanished. 55 | * [Arsylk](https://github.com/Arsylk) - Live2D viewer implementation, [mods forum](https://arsylk.pythonanywhere.com/apk/view_models), swapping, and much more 56 | * TinyBanana 57 | * WhoCares8128 58 | * Envy 59 | * Eljoseto - Site icon 60 | 61 | [Icons](https://materialdesignicons.com/) 62 | 63 | ## Development 64 | 65 | * Seems to work best with [JDK v8](https://www.oracle.com/java/technologies/javase/javase-jdk8-downloads.html). 66 | * or [JDK v11](https://www.oracle.com/java/technologies/javase-jdk11-downloads.html). 67 | * Download and install [Android Development Studio](https://developer.android.com/studio) 68 | * Use Android Studio SDK Manager to install Android SDK v28 69 | * Set ANDROID_SDK_ROOT environment variable to the path of the SDK (e.g. C:\Users\USERNAME\AppData\Local\Android\Sdk) 70 | * Accept licences by running $ANDROID_SDK_ROOT/tools/bin/sdkmanager --licenses (sdkmanager.bat on Windows) -------------------------------------------------------------------------------- /src/lib/installed.js: -------------------------------------------------------------------------------- 1 | import RNFS from 'react-native-fs' 2 | import {getInstalledPath, getCharactersPath} from './paths' 3 | import store from './store' 4 | import modelInfo from '../reducers/model-info'; 5 | import RNFetchBlob from 'rn-fetch-blob' 6 | import {loadInstalled} from '../actions/installed'; 7 | import {setLoading} from '../actions/loading' 8 | import { pushView } from '../actions/view'; 9 | import swap from '../lib/swap' 10 | 11 | export const readInstalled = () => { 12 | return RNFS.exists(getInstalledPath()).then(exists => exists 13 | ? RNFS.readFile(getInstalledPath()) 14 | .then(installed => JSON.parse(installed)) 15 | .catch(e => console.log('error', e)) 16 | : {} 17 | ) 18 | } 19 | 20 | 21 | export const writeInstalled = installed => { 22 | return RNFetchBlob.fs.writeFile(getInstalledPath(), JSON.stringify(installed, null, 2), 'utf8') 23 | } 24 | 25 | export const detectInstalled = () => { 26 | store.dispatch(setLoading(true, {title: 'Detecting installed mods'})) 27 | let numLoaded = 0 28 | readInstalled().then(installed => { 29 | const {mods, characters} = store.getState() 30 | RNFetchBlob.fs.ls(getCharactersPath()).then(files => { 31 | const hashPromises = [], 32 | pckFiles = files.filter(file => file.match(/\.pck$/)) 33 | let i = -1; 34 | const processNextHash = () => { 35 | i++ 36 | const file = pckFiles[i], 37 | target = file.replace(/\.pck$/, ''), 38 | [targetCode, targetVariant] = target.split('_') 39 | store.dispatch(setLoading(true, {title: 'Detecting installed mods', 40 | message: 'Processing ' + file + ' ...', 41 | progress: i, total: pckFiles.length 42 | })) 43 | RNFetchBlob.fs.hash(getCharactersPath() + file, 'md5') 44 | .then(hash => { 45 | if(i == pckFiles.length - 1) { 46 | store.dispatch(setLoading(true, {title: 'Detecting installed mods', message: 'Saving installed mods ...'})) 47 | writeInstalled(installed).then(() => { 48 | store.dispatch(setLoading(false)) 49 | store.dispatch(loadInstalled()) 50 | store.dispatch(pushView('installed')) 51 | }) 52 | } 53 | else { 54 | const mod = mods[hash], 55 | {code, variant} = mod || {} 56 | if(mod && characters[targetCode] && characters[targetCode].variants[targetVariant] && characters[targetCode].variants[targetVariant].mods[0] != hash) { 57 | installed[target] = {hash} 58 | processNextHash() 59 | } 60 | else { 61 | RNFS.copyFile(getCharactersPath() + file, getCharactersPath() + 'c999_00.pck') 62 | .then(() => { 63 | swap( 64 | getCharactersPath() + file, 65 | getCharactersPath() + 'c999_00.pck' 66 | ).then(() => { 67 | RNFS.readDir(RNFS.DocumentDirectoryPath + '/tmp/swap_target').then(dirs => { 68 | const hashPromises = [] 69 | let textureHash = '' 70 | dirs.map(({name}) => name).filter(name => name.match(/\.png$/)).forEach(name => { 71 | hashPromises.push( 72 | RNFetchBlob.fs.hash(RNFS.DocumentDirectoryPath + '/tmp/swap_target/' + name, 'md5').then(hash => textureHash += hash) 73 | ) 74 | }) 75 | Promise.all(hashPromises).then(() => { 76 | const hash = Object.keys(mods).find(hash => { 77 | const mod = mods[hash] 78 | return mod.textureHash == textureHash 79 | }) 80 | if(hash && characters[targetCode] && characters[targetCode].variants[targetVariant] && characters[targetCode].variants[targetVariant].mods[0] != hash) installed[target] = {hash} 81 | processNextHash() 82 | }) 83 | }) 84 | }) 85 | }) 86 | } 87 | } 88 | }) 89 | } 90 | processNextHash() 91 | // store.dispatch(setLoading('Detecting installed mods', '', 0, pckFiles.length)) 92 | // hashPromises.push( 93 | // RNFetchBlob.fs.hash(getCharactersPath() + file, 'md5') 94 | // .then(hash => { 95 | // numLoaded++ 96 | // store.dispatch(setLoading('Detecting installed mods', '', numLoaded, pckFiles.length)) 97 | // if(mods[hash] && characters[targetCode] && characters[targetCode].variants[targetVariant]) { 98 | // installed[target] = {hash} 99 | // } 100 | // }) 101 | // ) 102 | // }) 103 | // Promise.all(hashPromises).then(() => { 104 | // writeInstalled(installed).then(() => { 105 | // store.dispatch(setLoading(false)) 106 | // store.dispatch(loadInstalled()) 107 | // }) 108 | // }) 109 | }) 110 | }) 111 | } -------------------------------------------------------------------------------- /src/InstalledPreview.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {connect} from 'react-redux' 3 | import {View, TouchableHighlight} from 'react-native' 4 | import ScaledImage from './ScaledImage' 5 | import {install} from './actions/mods' 6 | import ModderCreditLink from './ModderCreditLink' 7 | import {pushView} from './actions/view' 8 | import {Button, Card, List, Text, TouchableRipple, IconButton, Paragraph} from 'react-native-paper' 9 | 10 | function ModPreview({mod, hash, pushView, character, target, characters, removeFromList, installed, install, mods}) { 11 | const {code, variant} = mod, 12 | [targetCode, targetVariant] = target.split('_'), 13 | targetCharacter = characters[targetCode], 14 | targetHash = targetCharacter.variants[targetVariant].mods[0], 15 | isInstalled = installed[target] && hash == installed[target].hash, 16 | isDefaultInstalled = isInstalled && characters[targetCode].variants[targetVariant].mods.indexOf(hash) == 0, 17 | installedMod = installed[target] && installed[target].hash && Object.assign({}, mods[installed[target].hash], {hash: installed[target].hash}) 18 | mod = Object.assign({}, mod, {hash}) 19 | return (code && character) 20 | ? ( 21 | 22 | 23 | {/* */} 24 | {/* */} 27 | 28 | {removeFromList && 29 | 34 | } 35 | 36 | {targetHash && 37 | 38 | pushView('mod', {hash: targetHash})} style={{alignItems: 'center'}}> 39 | <> 40 | 45 | {target} 46 | 47 | 48 | 49 | {installedMod && installedMod.hash != hash && 50 | pushView('mod', {hash: installedMod.hash})} style={{alignItems: 'center'}}> 51 | <> 52 | 57 | currently 58 | installed 59 | 60 | 61 | } 62 | 63 | } 64 | pushView('mod', {hash})} style={{marginLeft: targetHash ? 20 : 0}}> 65 | 69 | 70 | 71 | 72 | 73 | 92 | 93 | 94 | ) 95 | : null 96 | } 97 | 98 | export default connect( 99 | ({mods, installed, characters}, {hash}) => { 100 | const mod = mods[hash] || {} 101 | return { 102 | mod, 103 | character: mods[hash] && characters[mods[hash].code], 104 | hash, 105 | characters, 106 | installed, 107 | mods 108 | } 109 | }, 110 | ({pushView, install}) 111 | )(ModPreview) -------------------------------------------------------------------------------- /src/Characters.js: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react' 2 | import {connect} from 'react-redux' 3 | import { 4 | View, 5 | Button 6 | } from 'react-native' 7 | import {TextInput, Menu, DataTable, Headline} from 'react-native-paper' 8 | import ScaledImage from './ScaledImage' 9 | import ModPreview from './ModPreview' 10 | import CharacterPreview from './CharacterPreview' 11 | import {pushView} from './actions/view' 12 | 13 | function Mods({mods, characters, config, view, pushView}) { 14 | if(!config.defaultCharacterSortOrder) return null 15 | const [filter, setFilter] = useState(''), 16 | [sortMenuVisible, setSortMenuVisible] = useState(false), 17 | [showMenuVisible, setShowMenuVisible] = useState(false), 18 | page = view.data.page || 0, 19 | show = view.data.show || config.defaultCharacterShow, 20 | sort = view.data.sort || config.defaultCharacterSortOrder, 21 | setPage = p => pushView('characters', {...view.data, page: p}), 22 | setSortAndClose = val => { 23 | pushView('characters', {...view.data, page: 0, sort: val}), 24 | setSortMenuVisible(false) 25 | }, 26 | setShowAndClose = val => { 27 | pushView('characters', {...view.data, page: 0, show: val}), 28 | setShowMenuVisible(false) 29 | }, 30 | itemsPerPage = 10 31 | let filtered = Object.keys(characters).reduce((acc, code) => { 32 | if(!filter || (`${characters[code].name} ${characters[code].code}`).toLowerCase().match(filter.toLowerCase())) { 33 | acc.push(characters[code]) 34 | } 35 | return acc 36 | }, []) 37 | // if(sort == 'recently added') { 38 | // filtered = filtered.sort((a, b) => a.created > b.created ? -1 : b.created > a.created ? 1 : 0) 39 | // } 40 | // if(sort == 'oldest') { 41 | // filtered = filtered.sort((a, b) => a.created < b.created ? -1 : b.created < a.created ? 1 : 0) 42 | // } 43 | if(show === 'childs') { 44 | filtered = filtered.filter(({code}) => code.match(/^c\d\d\d/)) 45 | } 46 | if(show === 'spa childs') { 47 | filtered = filtered.filter(({code}) => code.match(/^sc\d\d\d/)) 48 | } 49 | if(show === 'monsters') { 50 | filtered = filtered.filter(({code}) => code.match(/^m\d\d\d/)) 51 | } 52 | if(show === 'spa monsters') { 53 | filtered = filtered.filter(({code}) => code.match(/^sm\d\d\d/)) 54 | } 55 | if(show === 'other') { 56 | filtered = filtered.filter(({code}) => code.match(/^v\d\d\d/)) 57 | } 58 | if(sort == 'name') { 59 | filtered = filtered.sort((a, b) => { 60 | a = a.name 61 | b = b.name 62 | return a < b ? -1 : b < a ? 1 : 0 63 | }) 64 | } 65 | if(sort == 'code' || sort == 'code-desc') { 66 | filtered = filtered.sort((a, b) => { 67 | a = a.code 68 | b = b.code 69 | return a < b ? -1 : b < a ? 1 : 0 70 | }) 71 | } 72 | if(sort.match('-desc')) filtered.reverse() 73 | const from = page * itemsPerPage, 74 | to = Math.min((page + 1) * itemsPerPage, filtered.length), 75 | numPages = Math.ceil(filtered.length / itemsPerPage) 76 | return ( 77 | 78 | Characters 79 | 80 | setFilter(text)}/> 81 | 82 | 83 | setShowMenuVisible(false)} 87 | anchor={ 95 | 96 | 97 | setSortMenuVisible(false)} 101 | anchor={ 107 | 108 | 109 | setPage(page)} 113 | label={`${from + 1}-${to} of ${filtered.length}`} 114 | /> 115 | {filtered.slice(from, to).map(character => 116 | 117 | )} 118 | setPage(page)} 122 | label={`${from + 1}-${to} of ${filtered.length}`} 123 | /> 124 | 125 | ) 126 | } 127 | 128 | export default connect( 129 | ({mods, characters, config, view}) => ({mods, characters, config, view}), 130 | {pushView} 131 | )(Mods) -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | platform :ios, '9.0' 2 | require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules' 3 | 4 | def add_flipper_pods!(versions = {}) 5 | versions['Flipper'] ||= '~> 0.33.1' 6 | versions['DoubleConversion'] ||= '1.1.7' 7 | versions['Flipper-Folly'] ||= '~> 2.1' 8 | versions['Flipper-Glog'] ||= '0.3.6' 9 | versions['Flipper-PeerTalk'] ||= '~> 0.0.4' 10 | versions['Flipper-RSocket'] ||= '~> 1.0' 11 | 12 | pod 'FlipperKit', versions['Flipper'], :configuration => 'Debug' 13 | pod 'FlipperKit/FlipperKitLayoutPlugin', versions['Flipper'], :configuration => 'Debug' 14 | pod 'FlipperKit/SKIOSNetworkPlugin', versions['Flipper'], :configuration => 'Debug' 15 | pod 'FlipperKit/FlipperKitUserDefaultsPlugin', versions['Flipper'], :configuration => 'Debug' 16 | pod 'FlipperKit/FlipperKitReactPlugin', versions['Flipper'], :configuration => 'Debug' 17 | 18 | # List all transitive dependencies for FlipperKit pods 19 | # to avoid them being linked in Release builds 20 | pod 'Flipper', versions['Flipper'], :configuration => 'Debug' 21 | pod 'Flipper-DoubleConversion', versions['DoubleConversion'], :configuration => 'Debug' 22 | pod 'Flipper-Folly', versions['Flipper-Folly'], :configuration => 'Debug' 23 | pod 'Flipper-Glog', versions['Flipper-Glog'], :configuration => 'Debug' 24 | pod 'Flipper-PeerTalk', versions['Flipper-PeerTalk'], :configuration => 'Debug' 25 | pod 'Flipper-RSocket', versions['Flipper-RSocket'], :configuration => 'Debug' 26 | pod 'FlipperKit/Core', versions['Flipper'], :configuration => 'Debug' 27 | pod 'FlipperKit/CppBridge', versions['Flipper'], :configuration => 'Debug' 28 | pod 'FlipperKit/FBCxxFollyDynamicConvert', versions['Flipper'], :configuration => 'Debug' 29 | pod 'FlipperKit/FBDefines', versions['Flipper'], :configuration => 'Debug' 30 | pod 'FlipperKit/FKPortForwarding', versions['Flipper'], :configuration => 'Debug' 31 | pod 'FlipperKit/FlipperKitHighlightOverlay', versions['Flipper'], :configuration => 'Debug' 32 | pod 'FlipperKit/FlipperKitLayoutTextSearchable', versions['Flipper'], :configuration => 'Debug' 33 | pod 'FlipperKit/FlipperKitNetworkPlugin', versions['Flipper'], :configuration => 'Debug' 34 | end 35 | 36 | # Post Install processing for Flipper 37 | def flipper_post_install(installer) 38 | installer.pods_project.targets.each do |target| 39 | if target.name == 'YogaKit' 40 | target.build_configurations.each do |config| 41 | config.build_settings['SWIFT_VERSION'] = '4.1' 42 | end 43 | end 44 | end 45 | end 46 | 47 | target 'DcModManager' do 48 | # Pods for DcModManager 49 | pod 'FBLazyVector', :path => "../node_modules/react-native/Libraries/FBLazyVector" 50 | pod 'FBReactNativeSpec', :path => "../node_modules/react-native/Libraries/FBReactNativeSpec" 51 | pod 'RCTRequired', :path => "../node_modules/react-native/Libraries/RCTRequired" 52 | pod 'RCTTypeSafety', :path => "../node_modules/react-native/Libraries/TypeSafety" 53 | pod 'React', :path => '../node_modules/react-native/' 54 | pod 'React-Core', :path => '../node_modules/react-native/' 55 | pod 'React-CoreModules', :path => '../node_modules/react-native/React/CoreModules' 56 | pod 'React-Core/DevSupport', :path => '../node_modules/react-native/' 57 | pod 'React-RCTActionSheet', :path => '../node_modules/react-native/Libraries/ActionSheetIOS' 58 | pod 'React-RCTAnimation', :path => '../node_modules/react-native/Libraries/NativeAnimation' 59 | pod 'React-RCTBlob', :path => '../node_modules/react-native/Libraries/Blob' 60 | pod 'React-RCTImage', :path => '../node_modules/react-native/Libraries/Image' 61 | pod 'React-RCTLinking', :path => '../node_modules/react-native/Libraries/LinkingIOS' 62 | pod 'React-RCTNetwork', :path => '../node_modules/react-native/Libraries/Network' 63 | pod 'React-RCTSettings', :path => '../node_modules/react-native/Libraries/Settings' 64 | pod 'React-RCTText', :path => '../node_modules/react-native/Libraries/Text' 65 | pod 'React-RCTVibration', :path => '../node_modules/react-native/Libraries/Vibration' 66 | pod 'React-Core/RCTWebSocket', :path => '../node_modules/react-native/' 67 | 68 | pod 'React-cxxreact', :path => '../node_modules/react-native/ReactCommon/cxxreact' 69 | pod 'React-jsi', :path => '../node_modules/react-native/ReactCommon/jsi' 70 | pod 'React-jsiexecutor', :path => '../node_modules/react-native/ReactCommon/jsiexecutor' 71 | pod 'React-jsinspector', :path => '../node_modules/react-native/ReactCommon/jsinspector' 72 | pod 'ReactCommon/callinvoker', :path => "../node_modules/react-native/ReactCommon" 73 | pod 'ReactCommon/turbomodule/core', :path => "../node_modules/react-native/ReactCommon" 74 | pod 'Yoga', :path => '../node_modules/react-native/ReactCommon/yoga', :modular_headers => true 75 | 76 | pod 'DoubleConversion', :podspec => '../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec' 77 | pod 'glog', :podspec => '../node_modules/react-native/third-party-podspecs/glog.podspec' 78 | pod 'Folly', :podspec => '../node_modules/react-native/third-party-podspecs/Folly.podspec' 79 | 80 | pod 'RNVectorIcons', :path => '../node_modules/react-native-vector-icons' 81 | 82 | pod 'rn-fetch-blob', :path => '../node_modules/rn-fetch-blob' 83 | 84 | target 'DcModManagerTests' do 85 | inherit! :complete 86 | # Pods for testing 87 | end 88 | 89 | use_native_modules! 90 | 91 | # Enables Flipper. 92 | # 93 | # Note that if you have use_frameworks! enabled, Flipper will not work and 94 | # you should disable these next few lines. 95 | add_flipper_pods! 96 | post_install do |installer| 97 | flipper_post_install(installer) 98 | end 99 | end 100 | 101 | target 'DcModManager-tvOS' do 102 | # Pods for DcModManager-tvOS 103 | 104 | target 'DcModManager-tvOSTests' do 105 | inherit! :search_paths 106 | # Pods for testing 107 | end 108 | end 109 | -------------------------------------------------------------------------------- /src/List.js: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react' 2 | import {connect} from 'react-redux' 3 | import {View, TouchableHighlight} from 'react-native' 4 | import InstalledPreview from './InstalledPreview' 5 | import {Dialog, Portal, Paragraph, useTheme, Headline, DataTable, Subheading, Button, Card, TextInput} from 'react-native-paper' 6 | import {pushView} from './actions/view' 7 | import {installList} from './actions/mods' 8 | import {deleteList, setActiveList, removeModFromList} from './actions/lists' 9 | import ModLive2DPreview from './ModLive2DPreview' 10 | 11 | const List = ({pushView, view, characters, deleteList, setActiveList, activeList, removeModFromList, installList, isCommunityList}) => { 12 | const {list, community, page = 0} = view, 13 | {colors} = useTheme(), 14 | [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false), 15 | [confirmInstallOpen, setConfirmInstallOpen] = useState(false), 16 | [filter, setFilter] = useState(''), 17 | itemsPerPage = 10, 18 | modKeys = Object.keys(list.mods).sort(), 19 | filteredModKeys = filter.replace(/\s/g, '') 20 | ? modKeys.filter(key => { 21 | const [code, variant] = key.split('_') 22 | return (key + characters[code].variants[variant].title + ' ' + characters[code].name).toLowerCase().match(filter.toLowerCase()) 23 | }) 24 | : modKeys, 25 | from = page * itemsPerPage, 26 | to = Math.min((page + 1) * itemsPerPage, filteredModKeys.length), 27 | numPages = Math.ceil(filteredModKeys.length / itemsPerPage) 28 | return ( 29 | 30 | 31 | pushView('lists')}> 32 | Mod Lists 33 | 34 | > 35 | {list.name} 36 | 37 | 38 | {list.name} 39 | 40 | {list.description.replace(/\s/g, '') != '' && {list.description}} 41 | {!community 42 | ? <> 43 | 48 | 51 | 54 | 55 | : 58 | } 59 | 62 | Mods 63 | 68 | pushView('list', Object.assign({}, view, {page}))} 72 | label={`${from + 1}-${to} of ${filteredModKeys.length}`} 73 | /> 74 | 75 | {filteredModKeys.length 76 | ? filteredModKeys.slice(from, to).map(target => 77 | { 78 | removeModFromList(target, list) 79 | }}/> 80 | ) 81 | : There are no mods in this list yet. 82 | } 83 | pushView('list', Object.assign({}, view, {page}))} 87 | label={`${from + 1}-${to} of ${filteredModKeys.length}`} 88 | /> 89 | 90 | 91 | setConfirmDeleteOpen(false)}> 92 | Delete this list? 93 | 94 | Are you sure you want to delete the list called "{list.name}"? This CANNOT be undone. 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | setConfirmInstallOpen(false)}> 104 | Install all mods in this list? 105 | 106 | This could take around {Math.round(modKeys.length * .05)} minutes. 107 | Are you sure you want to install all {modKeys.length} mods in this list? The process shouldn't be interrupted, so be prepared to leave your phone alone, and maybe plugged in while the list installs. 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | ) 117 | } 118 | 119 | export default connect( 120 | state => ({ 121 | characters: state.characters, 122 | view: state.view.data, 123 | isCommunityList: state.view.data.community, 124 | activeList: state.lists.active 125 | }), 126 | ({pushView, deleteList, setActiveList, removeModFromList, installList}) 127 | )(List) -------------------------------------------------------------------------------- /src/MainView.js: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useState} from 'react' 2 | import {connect, useStore} from 'react-redux' 3 | import {Paragraph, Dialog, Portal, useTheme, Button, Text, IconButton} from 'react-native-paper' 4 | import Character from './Character' 5 | import ScrollTop from './ScrollTop'; 6 | import Characters from './Characters' 7 | import Mods from './Mods' 8 | import Mod from './Mod' 9 | import Variant from './Variant' 10 | import Settings from './Settings' 11 | import Tools from './Tools' 12 | import Modder from './Modder' 13 | import Modders from './Modders' 14 | import InstalledMods from './InstalledMods' 15 | import List from './List' 16 | import Lists from './Lists' 17 | import EditList from './EditList' 18 | import {popHistory} from './actions/history' 19 | import {pushView} from './actions/view' 20 | import {setActiveList} from './actions/lists' 21 | import {BackHandler, Dimensions, View, TouchableOpacity} from 'react-native' 22 | import ModelInfoEditor from './ModelInfoEditor'; 23 | 24 | function MainView({view, popHistory, pushView, loading, activeList, setActiveList}) { 25 | const store = useStore(), 26 | {colors} = useTheme(), 27 | [activeListHeight, setActiveListHeight] = useState(0), 28 | [activeListInfoOpen, setActiveListInfoOpen] = useState(false) 29 | useEffect(() => { 30 | const backAction = () => { 31 | const history = store.getState().history 32 | if(history.length > 1) { 33 | popHistory(history) 34 | return true 35 | } 36 | else return false 37 | }; 38 | const backHandler = BackHandler.addEventListener('hardwareBackPress', backAction) 39 | pushView('mods') 40 | return () => backHandler.remove(); 41 | }, []); 42 | let CurrentView 43 | switch(view.name) { 44 | case 'mod': CurrentView = Mod; break; 45 | case 'edit-list': CurrentView = EditList; break; 46 | case 'list': CurrentView = List; break; 47 | case 'lists': CurrentView = Lists; break; 48 | case 'modelInfoEditor': CurrentView = ModelInfoEditor; break; 49 | case 'modder': CurrentView = Modder; break; 50 | case 'modders': CurrentView = Modders; break; 51 | case 'installed': CurrentView = InstalledMods; break; 52 | case 'character': CurrentView = Character; break; 53 | case 'characters': CurrentView = Characters; break; 54 | case 'characters': CurrentView = Characters; break; 55 | case 'settings': CurrentView = Settings; break; 56 | case 'tools': CurrentView = Tools; break; 57 | case 'variant': CurrentView = Variant; break; 58 | default: CurrentView = Mods; 59 | } 60 | return ( 61 | <> 62 | {loading.isLoading && ( 63 | 64 | 65 | {loading.title} 66 | 67 | {loading.message} 68 | {typeof loading.progress != 'undefined' && !!loading.total && 69 | 70 | 71 | 76 | 77 | 82 | {Math.round((loading.progress / loading.total) * 100)}% 83 | 84 | 85 | } 86 | 87 | 88 | 89 | )} 90 | {activeListInfoOpen && 91 | 92 | setActiveListInfoOpen(false)}> 93 | Active Mod List 94 | 95 | "{activeList.name}" is the currently active mod list. 96 | To add mods to this list, go to a mod and use the same procedure you would to install the mod to a specific character variant. This will add he mod to the list instead as long as there is an active list open. 97 | 98 | 99 | 100 | 101 | 102 | 103 | } 104 | 105 | 106 | 107 | {activeList && 108 | { 119 | var {x, y, width, height} = event.nativeEvent.layout 120 | setActiveListHeight(height) 121 | }}> 122 | pushView('list', {list: activeList})} 124 | icon="playlist-edit" 125 | style={{ 126 | position: 'absolute', 127 | top: 8, 128 | left: 0 129 | }} /> 130 | pushView('list', {list: activeList})}> 131 | 132 | {activeList.name} - {Object.keys(activeList.mods).length} mods 133 | 134 | 135 | 142 | setActiveListInfoOpen(true)} icon="information-outline" /> 143 | setActiveList(false)} icon="close" /> 144 | 145 | 146 | } 147 | 148 | ) 149 | } 150 | 151 | export default connect( 152 | ({view, loading, lists}) => ({ 153 | view, 154 | loading, 155 | activeList: lists.active 156 | }), 157 | {popHistory, pushView, setActiveList} 158 | )(MainView) -------------------------------------------------------------------------------- /android/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | # Determine the Java command to use to start the JVM. 86 | if [ -n "$JAVA_HOME" ] ; then 87 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 88 | # IBM's JDK on AIX uses strange locations for the executables 89 | JAVACMD="$JAVA_HOME/jre/sh/java" 90 | else 91 | JAVACMD="$JAVA_HOME/bin/java" 92 | fi 93 | if [ ! -x "$JAVACMD" ] ; then 94 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 95 | 96 | Please set the JAVA_HOME variable in your environment to match the 97 | location of your Java installation." 98 | fi 99 | else 100 | JAVACMD="java" 101 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 102 | 103 | Please set the JAVA_HOME variable in your environment to match the 104 | location of your Java installation." 105 | fi 106 | 107 | # Increase the maximum file descriptors if we can. 108 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 109 | MAX_FD_LIMIT=`ulimit -H -n` 110 | if [ $? -eq 0 ] ; then 111 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 112 | MAX_FD="$MAX_FD_LIMIT" 113 | fi 114 | ulimit -n $MAX_FD 115 | if [ $? -ne 0 ] ; then 116 | warn "Could not set maximum file descriptor limit: $MAX_FD" 117 | fi 118 | else 119 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 120 | fi 121 | fi 122 | 123 | # For Darwin, add options to specify how the application appears in the dock 124 | if $darwin; then 125 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 126 | fi 127 | 128 | # For Cygwin or MSYS, switch paths to Windows format before running java 129 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 130 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 131 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 132 | JAVACMD=`cygpath --unix "$JAVACMD"` 133 | 134 | # We build the pattern for arguments to be converted via cygpath 135 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 136 | SEP="" 137 | for dir in $ROOTDIRSRAW ; do 138 | ROOTDIRS="$ROOTDIRS$SEP$dir" 139 | SEP="|" 140 | done 141 | OURCYGPATTERN="(^($ROOTDIRS))" 142 | # Add a user-defined pattern to the cygpath arguments 143 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 144 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 145 | fi 146 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 147 | i=0 148 | for arg in "$@" ; do 149 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 150 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 151 | 152 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 153 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 154 | else 155 | eval `echo args$i`="\"$arg\"" 156 | fi 157 | i=$((i+1)) 158 | done 159 | case $i in 160 | (0) set -- ;; 161 | (1) set -- "$args0" ;; 162 | (2) set -- "$args0" "$args1" ;; 163 | (3) set -- "$args0" "$args1" "$args2" ;; 164 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 165 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 166 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 167 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 168 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 169 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 170 | esac 171 | fi 172 | 173 | # Escape application args 174 | save () { 175 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 176 | echo " " 177 | } 178 | APP_ARGS=$(save "$@") 179 | 180 | # Collect all arguments for the java command, following the shell quoting and substitution rules 181 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 182 | 183 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 184 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 185 | cd "$(dirname "$0")" 186 | fi 187 | 188 | exec "$JAVACMD" "$@" 189 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/arsylk/mammonsmite/Live2D/L2DModel.java: -------------------------------------------------------------------------------- 1 | package com.arsylk.mammonsmite.Live2D; 2 | 3 | import com.arsylk.mammonsmite.DestinyChild.DCModel; 4 | import com.arsylk.mammonsmite.DestinyChild.DCModelInfo; 5 | import com.arsylk.mammonsmite.utils.Utils; 6 | import org.apache.commons.io.FileUtils; 7 | import org.json.JSONObject; 8 | 9 | import java.io.*; 10 | import java.nio.charset.Charset; 11 | import java.util.*; 12 | 13 | public class L2DModel { 14 | private DCModel.DCModelJson modelJson; 15 | private File output; 16 | private String modelName, modelIdx; 17 | private JSONObject _modelJson = null, infoJson = null, infoBakJson = null; 18 | 19 | //constructors 20 | public L2DModel(String model) { 21 | load(new File(model)); 22 | } 23 | 24 | public L2DModel(File model) { 25 | load(model); 26 | } 27 | 28 | public L2DModel(File output, DCModel.DCModelJson modelJson) { 29 | this.output = output; 30 | this.modelJson = modelJson; 31 | this.modelIdx = modelJson.getModelIdx(); 32 | this.modelName = DCModelInfo.getInstance().getModelFull(modelIdx); 33 | } 34 | 35 | 36 | //methods 37 | private void load(File model) { 38 | //fail safe 39 | if(model.isDirectory()) 40 | model = new File(model, "model.json"); 41 | modelJson = new DCModel.DCModelJson(Utils.fileToJson(model)); 42 | 43 | //only if correct model 44 | if(modelJson.isLoaded()) { 45 | modelIdx = modelJson.getModelIdx(); 46 | output = model.getParentFile(); 47 | 48 | if(getModelConfig().exists()) { 49 | //load from saved file 50 | _modelJson = Utils.fileToJson(getModelConfig()); 51 | if(_modelJson != null) { 52 | if(_modelJson.has("model_name") && _modelJson.has("model_id")) { 53 | try { 54 | modelIdx = _modelJson.getString("model_id"); 55 | modelName = _modelJson.getString("model_name"); 56 | }catch(Exception e) { 57 | e.printStackTrace(); 58 | } 59 | } 60 | if(_modelJson.has("model_info")) { 61 | try { 62 | infoJson = _modelJson.getJSONObject("model_info"); 63 | infoBakJson = _modelJson.getJSONObject("model_info_bak"); 64 | }catch(Exception e) { 65 | return; 66 | } 67 | } 68 | } 69 | }else { 70 | //load default params 71 | modelName = DCModelInfo.getInstance().getModelFull(modelIdx); 72 | } 73 | } 74 | } 75 | 76 | public synchronized void generateModel() { 77 | try{ 78 | //generate json 79 | JSONObject _model = new JSONObject() 80 | .put("model_id", modelIdx) 81 | .put("model_name", modelName); 82 | if(infoJson != null) { 83 | _model.put("model_info", infoJson); 84 | } 85 | if(infoBakJson != null) { 86 | _model.put("model_info_bak", infoBakJson); 87 | } 88 | 89 | //write json to file 90 | FileUtils.write(getModelConfig(), _model.toString(4), Charset.forName("utf-8")); 91 | }catch(Exception e) { 92 | e.printStackTrace(); 93 | } 94 | } 95 | 96 | 97 | //setters 98 | public void setOutput(File output) { 99 | this.output = output; 100 | } 101 | 102 | public void setModelName(String modelName) { 103 | this.modelName = modelName; 104 | } 105 | 106 | public void setModelInfoJson(JSONObject infoJson) { 107 | this.infoJson = infoJson; 108 | } 109 | 110 | public void setModelInfoBakJson(JSONObject infoBakJson) { 111 | this.infoBakJson = infoBakJson; 112 | } 113 | 114 | 115 | //getters 116 | public boolean isLoaded() { 117 | return modelJson.isLoaded() && modelIdx != null; 118 | } 119 | 120 | public String getModelId() { 121 | return modelIdx; 122 | } 123 | 124 | public String getModelName() { 125 | return modelName != null ? modelName : getModelId(); 126 | } 127 | 128 | public JSONObject getModelConfigJson() { 129 | return _modelJson != null ? _modelJson : new JSONObject(); 130 | } 131 | 132 | public JSONObject getModelInfoJson() { 133 | return infoJson != null ? infoJson : new JSONObject(); 134 | } 135 | 136 | public JSONObject getModelInfoBakJson() { 137 | return infoBakJson != null ? infoBakJson : new JSONObject(); 138 | } 139 | 140 | //getters files 141 | public File getOutput() { 142 | return output; 143 | } 144 | 145 | public File getModelHeader() { 146 | return new File(output, "_header"); 147 | } 148 | 149 | public File getModelConfig() { 150 | return new File(output, "_model"); 151 | } 152 | 153 | public File getModel() { 154 | return new File(output, "model.json"); 155 | } 156 | 157 | public File getCharacter() { 158 | return new File(output, modelJson.getModel()); 159 | } 160 | 161 | public File[] getTextures() { 162 | File[] textures = new File[modelJson.getTextures().length]; 163 | for(int i = 0; i < modelJson.getTextures().length; i++) { 164 | textures[i] = new File(output, modelJson.getTextures()[i]); 165 | } 166 | return textures; 167 | } 168 | 169 | public File[] getMotions() { 170 | List motions = new ArrayList<>(modelJson.getMotions().values()); 171 | File[] motionFiles = new File[motions.size()]; 172 | for(int i = 0; i < motionFiles.length; i++) { 173 | motionFiles[i] = new File(output, motions.get(i)); 174 | } 175 | return motionFiles; 176 | } 177 | 178 | public File getMotion(String name) { 179 | if(modelJson.getMotions().containsKey(name)) { 180 | return new File(output, modelJson.getMotions().get(name)); 181 | } 182 | return null; 183 | } 184 | 185 | public File[] getExpressions() { 186 | List expressions = new ArrayList<>(modelJson.getExpressions().values()); 187 | File[] expressionFiles = new File[expressions.size()]; 188 | for(int i = 0; i < expressionFiles.length; i++) { 189 | expressionFiles[i] = new File(output, expressions.get(i)); 190 | } 191 | return expressionFiles; 192 | } 193 | 194 | public File getExpression(String name) { 195 | if(modelJson.getExpressions().containsKey(name)) { 196 | return new File(output, modelJson.getExpressions().get(name)); 197 | } 198 | return null; 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/arsylk/mammonsmite/utils/Utils.java: -------------------------------------------------------------------------------- 1 | package com.arsylk.mammonsmite.utils; 2 | 3 | import android.Manifest; 4 | import android.annotation.SuppressLint; 5 | import android.app.Activity; 6 | import android.content.Context; 7 | import android.content.SharedPreferences; 8 | import android.content.pm.PackageManager; 9 | import android.graphics.Bitmap; 10 | import android.graphics.Color; 11 | import android.media.AudioManager; 12 | import android.media.MediaPlayer; 13 | import android.net.Uri; 14 | import android.os.Build; 15 | import android.os.Environment; 16 | import android.preference.PreferenceManager; 17 | import androidx.core.app.ActivityCompat; 18 | import androidx.core.content.ContextCompat; 19 | import android.view.View; 20 | import java.io.InputStream; 21 | import android.view.inputmethod.InputMethodManager; 22 | import org.apache.commons.io.FileUtils; 23 | import org.json.JSONObject; 24 | import org.jsoup.Jsoup; 25 | import org.jsoup.nodes.Document; 26 | // import android.os.FileUtils; 27 | import javax.crypto.BadPaddingException; 28 | import javax.crypto.Cipher; 29 | import javax.crypto.IllegalBlockSizeException; 30 | import javax.crypto.NoSuchPaddingException; 31 | import javax.crypto.spec.SecretKeySpec; 32 | import java.io.*; 33 | import java.nio.*; 34 | import java.nio.charset.StandardCharsets; 35 | import java.security.InvalidKeyException; 36 | import java.security.MessageDigest; 37 | import java.security.NoSuchAlgorithmException; 38 | import java.text.SimpleDateFormat; 39 | import java.util.*; 40 | 41 | public class Utils { 42 | 43 | /*yappy start*/ 44 | private static int[][] yappy_maps = new int[32][16]; 45 | private static int[] yappy_info = new int[256]; 46 | private static boolean yappy_mapped = false; 47 | 48 | private static void yappy_fill() { 49 | long step = 1 << 16; 50 | for(int i = 0; i < 16; ++i) { 51 | int value = 65535; 52 | step = ((step * 67537) >> 16); 53 | while(value < (29L << 16)) { 54 | yappy_maps[value >> 16][i] = 1; 55 | value = (int) ((value * step) >> 16); 56 | } 57 | } 58 | 59 | int cntr = 0; 60 | for(int i = 0; i < 29; ++i) { 61 | for(int j = 0; j < 16; ++j) { 62 | if(yappy_maps[i][j] != 0) { 63 | yappy_info[32 + cntr] = i + 4 + (j << 8); 64 | yappy_maps[i][j] = 32 + cntr; 65 | cntr += 1; 66 | }else { 67 | if(i == 0) 68 | throw new EmptyStackException(); 69 | yappy_maps[i][j] = yappy_maps[i - 1][j]; 70 | } 71 | } 72 | } 73 | if(cntr != 256 - 32) { 74 | throw new EmptyStackException(); 75 | } 76 | yappy_mapped = true; 77 | } 78 | 79 | public static byte[] yappy_uncompress(byte[] data, int size) { 80 | if(!yappy_mapped) 81 | yappy_fill(); 82 | 83 | ArrayList to = new ArrayList<>(); 84 | int data_p = 0; 85 | int to_p = 0; 86 | while(to.size() < size) { 87 | if(!(data_p + 1 < data.length)) 88 | return data; 89 | 90 | int index = data[data_p] & 0xFF; 91 | if(index < 32) { 92 | byte[] copy = Arrays.copyOfRange(data, data_p+1, (data_p+1)+(index+1)); 93 | for(byte byte_copy : copy) { 94 | to.add(byte_copy); 95 | } 96 | to_p += index + 1; 97 | data_p += index + 2; 98 | }else { 99 | int info = yappy_info[index]; 100 | int length = info & 0x00ff; 101 | int offset = (info & 0xff00) + (data[data_p+1] & 0xFF); 102 | List copy = to.subList((to_p - offset), Math.min((to_p - offset)+length, to.size())); 103 | to.addAll(copy); 104 | to_p += length; 105 | data_p += 2; 106 | } 107 | } 108 | 109 | byte[] to_byte = new byte[to.size()]; 110 | for(int i = 0; i < to.size(); i++) { 111 | to_byte[i] = to.get(i); 112 | } 113 | return to_byte; 114 | } 115 | /*yappy end*/ 116 | 117 | /*aes start*/ 118 | private static Cipher[] cipher = {null, null}; 119 | private static boolean[] cipher_made = {false, false}; 120 | 121 | private static void make_cipher(int key) throws InvalidKeyException, NoSuchPaddingException, NoSuchAlgorithmException { 122 | if(key == 0) { 123 | byte[] key0 = new byte[] {(byte) 0x37, (byte) 0xea, (byte) 0x79, (byte) 0x85, (byte) 0x86, (byte) 0x29, (byte) 0xec, (byte) 0x94, (byte) 0x85, (byte) 0x20, (byte) 0x7c, (byte) 0x1a, (byte) 0x62, (byte) 0xc3, (byte) 0x72, (byte) 0x4f, (byte) 0x72, (byte) 0x75, (byte) 0x25, (byte) 0x0b, (byte) 0x99, (byte) 0x99, (byte) 0xbd, (byte) 0x7f, (byte) 0x0b, (byte) 0x24, (byte) 0x9a, (byte) 0x8d, (byte) 0x85, (byte) 0x38, (byte) 0x0e, (byte) 0x39}; 124 | cipher[key] = Cipher.getInstance("AES/ECB/NoPadding"); 125 | cipher[key].init(Cipher.DECRYPT_MODE, new SecretKeySpec(key0, "AES")); 126 | cipher_made[key] = true; 127 | }else if(key == 1) { 128 | byte[] key1 = new byte[] {(byte) 0xEF, (byte) 0xBB, (byte) 0xBF, (byte) 0xEC, (byte) 0x8B, (byte) 0x9C, (byte) 0xED, (byte) 0x94, (byte) 0x84, (byte) 0xED, (byte) 0x8A, (byte) 0xB8, (byte) 0xEC, (byte) 0x97, (byte) 0x85, (byte) 0xEA, (byte) 0xB3, (byte) 0xBC, (byte) 0xEB, (byte) 0x9D, (byte) 0xBC, (byte) 0xEC, (byte) 0x9D, (byte) 0xB8, (byte) 0xEA, (byte) 0xB2, (byte) 0x8C, (byte) 0xEC, (byte) 0x9E, (byte) 0x84, (byte) 0xEC, (byte) 0xA6}; 129 | cipher[key] = Cipher.getInstance("AES/ECB/NoPadding"); 130 | cipher[key].init(Cipher.DECRYPT_MODE, new SecretKeySpec(key1, "AES")); 131 | cipher_made[key] = true; 132 | } 133 | 134 | } 135 | 136 | public static byte[] aes_decrypt(byte[] data, int key) throws BadPaddingException, IllegalBlockSizeException, NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException { 137 | if(!cipher_made[key]) 138 | make_cipher(key); 139 | 140 | //16 byte blocks 141 | data = Arrays.copyOf(data, data.length+(16 - (data.length % 16))); 142 | 143 | return cipher[key].doFinal(data); 144 | } 145 | /*aes end*/ 146 | 147 | public static String bytesToHex(byte[] bytes) { 148 | char[] hexArray = {'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'}; 149 | char[] hexChars = new char[bytes.length * 2]; 150 | for(int i = 0; i < bytes.length; i++) { 151 | int v = bytes[i] & 0xFF; 152 | hexChars[i * 2] = hexArray[v >>> 4]; 153 | hexChars[i * 2 + 1] = hexArray[v & 0x0F]; 154 | } 155 | return new String(hexChars); 156 | } 157 | 158 | public static byte[] hexToBytes(String hex) { 159 | int len = hex.length(); 160 | if(len % 2 == 0) { 161 | byte[] data = new byte[len/2]; 162 | for(int i = 0; i < len; i+=2) { 163 | data[i/2] = ((byte)((Character.digit(hex.charAt(i), 16) << 4) + Character.digit(hex.charAt(i+1), 16))); 164 | } 165 | return data; 166 | } 167 | return null; 168 | } 169 | 170 | public static File rename(File file, String newName) { 171 | File rename = new File(file.getParent(), newName); 172 | if(file.renameTo(rename)) { 173 | file = rename; 174 | } 175 | return file; 176 | } 177 | 178 | /*json start*/ 179 | @SuppressLint("NewApi") 180 | public static JSONObject fileToJson(InputStream in) { 181 | try { 182 | BufferedReader br = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8)); 183 | StringBuilder content = new StringBuilder(); 184 | String line; 185 | while((line = br.readLine()) != null) 186 | content.append(line); 187 | br.close(); 188 | return new JSONObject(content.toString()); 189 | } catch (Exception e) { 190 | e.printStackTrace(); 191 | } 192 | return null; 193 | } 194 | 195 | public static JSONObject fileToJson(File file) { 196 | try { 197 | BufferedReader br = new BufferedReader(new FileReader(file)); 198 | StringBuilder content = new StringBuilder(); 199 | String line; 200 | while((line = br.readLine()) != null) 201 | content.append(line); 202 | br.close(); 203 | return new JSONObject(content.toString()); 204 | } catch (Exception e) { 205 | e.printStackTrace(); 206 | } 207 | return null; 208 | } 209 | /*json end*/ 210 | } 211 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Sample React Native App 3 | * https://github.com/facebook/react-native 4 | * 5 | * @format 6 | * @flow strict-local 7 | */ 8 | 9 | import React, {useEffect, useState, version} from 'react'; 10 | import { 11 | StyleSheet, 12 | ScrollView, 13 | View, 14 | Linking, 15 | StatusBar, 16 | Alert 17 | } from 'react-native'; 18 | 19 | import {Provider} from 'react-redux' 20 | import {check, request, PERMISSIONS, RESULTS} from 'react-native-permissions'; 21 | import {Portal, Paragraph, Button, Dialog, DefaultTheme, Provider as PaperProvider, Appbar, Menu} from 'react-native-paper'; 22 | import store from './lib/store' 23 | import {fetchMods} from './actions/mods' 24 | import {fetchModelInfo} from './actions/model-info' 25 | import {fetchCharacters} from './actions/characters' 26 | import {loadConfig} from './actions/config' 27 | import {loadInstalled} from './actions/installed' 28 | import {loadLists} from './actions/lists' 29 | import MainView from './MainView' 30 | import {getDcModManagerFolderPath} from './lib/paths' 31 | import ScrollTop from './ScrollTop'; 32 | import DCTools from './DCTools' 33 | import Drawer from './Drawer' 34 | import RNFS from 'react-native-fs' 35 | import {pushView} from './actions/view'; 36 | import {fetchModders} from './actions/modders'; 37 | import ScaledImage from './ScaledImage' 38 | import {checkForUpdate, installUpdate} from './lib/update' 39 | 40 | const theme = { 41 | ...DefaultTheme, 42 | roundness: 4, 43 | dark: true, 44 | colors: { 45 | ...DefaultTheme.colors, 46 | primary: '#facf32', 47 | // secondary: '#dd9200', 48 | accent: '#f1c40f', 49 | background: '#111', 50 | paper: '#222', 51 | surface: '#222', 52 | text: '#eee', 53 | placeholder: '#aaa' 54 | }, 55 | }; 56 | 57 | let readExternalStorageRequested = false 58 | 59 | function App() { 60 | 61 | const [readExternalStorageGranted, setReadExternalStorageGranted] = useState(false), 62 | [menuOpen, setMenuOpen] = useState(false), 63 | [drawerOpen, setDrawerOpen] = useState(false), 64 | [update, setUpdate] = useState(false), 65 | loadInitialData = () => { 66 | store.dispatch(loadConfig()) 67 | store.dispatch(fetchMods()) 68 | store.dispatch(fetchModders()) 69 | store.dispatch(fetchCharacters()) 70 | store.dispatch(fetchModelInfo()) 71 | store.dispatch(loadLists()) 72 | DCTools.setTmpPath(RNFS.DocumentDirectoryPath + '/tmp') 73 | DCTools.setAppsPath(RNFS.ExternalStorageDirectoryPath + '/Android/data') 74 | checkForUpdate().then(setUpdate) 75 | }, 76 | continueAfterPermissionGranted = () => { 77 | setReadExternalStorageGranted(true) 78 | RNFS.exists(getDcModManagerFolderPath()) 79 | .then(exists => exists 80 | ? loadInitialData() 81 | : RNFS.mkdir(getDcModManagerFolderPath()).then(loadInitialData) 82 | ) 83 | 84 | }, 85 | requestReadExternalStoragePermission = () => 86 | request(PERMISSIONS.ANDROID.WRITE_EXTERNAL_STORAGE).then((result) => { 87 | checkReadExternalStorageGranted() 88 | }), 89 | checkReadExternalStorageGranted = () => { 90 | check(PERMISSIONS.ANDROID.WRITE_EXTERNAL_STORAGE) 91 | .then(result => { 92 | switch (result) { 93 | case RESULTS.UNAVAILABLE: 94 | Alert.alert( 95 | 'Permission Error', 96 | 'DC Mod Manager will not work on this device since you cannot grant read external storage permission.', 97 | [], 98 | {cancelable: false} 99 | ) 100 | break; 101 | case RESULTS.DENIED: 102 | console.log('Read external storage permission denied') 103 | if(readExternalStorageRequested) { 104 | Alert.alert( 105 | 'Permission Error', 106 | 'DC Mod Manager will not work or be able to install mods if it cannot read and write to the Destiny Child app folder in your external storage.', 107 | [ 108 | {text: 'OK', onPress: () => requestReadExternalStoragePermission()} 109 | ], 110 | {cancelable: false} 111 | ); 112 | } 113 | else { 114 | readExternalStorageRequested = true 115 | requestReadExternalStoragePermission() 116 | } 117 | break; 118 | case RESULTS.GRANTED: 119 | continueAfterPermissionGranted() 120 | break; 121 | case RESULTS.BLOCKED: 122 | console.log('The permission is denied and not requestable anymore'); 123 | break; 124 | } 125 | }) 126 | .catch((error) => { 127 | // … 128 | }); 129 | } 130 | 131 | useEffect(() => checkReadExternalStorageGranted(), []); 132 | return readExternalStorageGranted 133 | ? ( 134 | 135 | 136 | 137 | setDrawerOpen(!drawerOpen)} /> 138 | {/* store.dispatch(pushView(store.getState().config.defaultView)) }> 139 | 140 | */} 141 | { 142 | setDrawerOpen(false) 143 | store.dispatch(pushView(store.getState().config.defaultView)) 144 | }}/> 145 | setMenuOpen(false)} 148 | style={{marginTop: 56}} 149 | anchor={ { 150 | setDrawerOpen(false) 151 | setMenuOpen(true) 152 | }} />}> 153 | { 154 | setMenuOpen(false) 155 | Linking.openURL('https://github.com/PhasmaExMachina/dc-mod-manager/blob/master/README.md#dc-mod-manager-app-for-android') 156 | }} title="About" /> 157 | { 158 | setMenuOpen(false) 159 | Linking.openURL('https://github.com/PhasmaExMachina/dc-mod-manager/issues') 160 | }} title="Feedback" /> 161 | { 162 | setMenuOpen(false) 163 | Linking.openURL('https://github.com/PhasmaExMachina/dc-mod-manager/releases') 164 | }} title="Releases" /> 165 | { 166 | setMenuOpen(false) 167 | Linking.openURL('https://github.com/PhasmaExMachina/dc-mod-manager/blob/master/README.md#credits') 168 | }} title="Credits" /> 169 | { 170 | setMenuOpen(false) 171 | Linking.openURL('https://github.com/PhasmaExMachina/dc-mod-manager') 172 | }} title="Fork on GitHub" /> 173 | 174 | {/* {}} /> */} 175 | 176 | 177 | 178 | {/* */} 179 | 180 | {/* */} 181 | 182 | {drawerOpen && setDrawerOpen(false)} />} 183 | 184 | setUpdate(false)}> 185 | New update v{update.version} available 186 | 187 | {update.changelog && update.changelog.map(update => 188 | {update} 189 | )} 190 | 191 | 192 | 193 | 197 | 198 | 199 | 200 | 201 | 202 | ) 203 | : null 204 | }; 205 | 206 | const styles = StyleSheet.create({ 207 | scrollView: { 208 | backgroundColor: '#111', 209 | } 210 | }); 211 | 212 | export default App; 213 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/arsylk/mammonsmite/DestinyChild/Pck.java: -------------------------------------------------------------------------------- 1 | package com.arsylk.mammonsmite.DestinyChild; 2 | 3 | import android.util.Log; 4 | import com.arsylk.mammonsmite.utils.Utils; 5 | import org.json.JSONObject; 6 | 7 | import java.io.File; 8 | import java.io.FileOutputStream; 9 | import java.io.RandomAccessFile; 10 | import java.nio.ByteOrder; 11 | import java.nio.MappedByteBuffer; 12 | import java.nio.channels.FileChannel; 13 | import java.util.ArrayList; 14 | import java.util.Arrays; 15 | import java.util.List; 16 | 17 | import static com.arsylk.mammonsmite.DestinyChild.DCDefine.PCK_IDENTIFIER; 18 | 19 | public class Pck { 20 | public static class PckHeader { 21 | public class Item { 22 | public int length, i; 23 | public byte[] hash = new byte[8]; 24 | public String hashs; 25 | public int flag, offset, size, size_p; 26 | 27 | @Override 28 | public String toString() { 29 | return String.format("<%d/%d %s [%016X | %6d] %02d>", i+1, length, hashs, offset, size, flag); 30 | } 31 | } 32 | 33 | private File file; 34 | private boolean valid; 35 | private Item[] items = null; 36 | 37 | 38 | public PckHeader(File file) { 39 | this.file = file; 40 | valid = read(); 41 | } 42 | 43 | //methods 44 | private boolean read() { 45 | try { 46 | //buffer src bytes 47 | RandomAccessFile fs = new RandomAccessFile(file, "r"); 48 | MappedByteBuffer mbb = fs.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, fs.length()).load(); 49 | mbb.order(ByteOrder.LITTLE_ENDIAN); 50 | mbb.position(0); 51 | 52 | //begin byte analysis 53 | byte[] identifier = new byte[8]; 54 | //byte(8) pck identifier 55 | mbb.get(identifier); 56 | if(Arrays.equals(identifier, PCK_IDENTIFIER)) { 57 | //byte(4) count 58 | items = new Item[mbb.getInt()]; 59 | for(int i = 0; i < items.length; i++) { 60 | items[i] = new Item(); 61 | items[i].length = items.length; items[i].i = i; 62 | 63 | //byte(8) hash 64 | mbb.get(items[i].hash); 65 | items[i].hashs = Utils.bytesToHex(items[i].hash); 66 | //byte(1) flag 67 | items[i].flag = mbb.get(); 68 | //byte(4) offset 69 | items[i].offset = mbb.getInt(); 70 | //byte(4) compressed size 71 | items[i].size_p = mbb.getInt(); 72 | //byte(4) original size 73 | items[i].size = mbb.getInt(); 74 | //byte(4) ??? 75 | mbb.position(mbb.position()+4); 76 | } 77 | }else { 78 | return false; 79 | } 80 | mbb.clear(); 81 | fs.close(); 82 | 83 | return true; 84 | }catch(Exception e) { 85 | e.printStackTrace(); 86 | } 87 | return false; 88 | } 89 | 90 | //getters 91 | public File getFile() { 92 | return file; 93 | } 94 | 95 | public boolean isValid() { 96 | return valid; 97 | } 98 | 99 | public int getSize() { 100 | return items != null ? items.length : 0; 101 | } 102 | 103 | public Item[] getItems() { 104 | return items != null ? items : new Item[0]; 105 | } 106 | } 107 | 108 | public class PckFile { 109 | private File file; 110 | private byte[] hash; 111 | private int ext, index; 112 | 113 | public PckFile(File file, byte[] hash, int ext, int index) { 114 | this.file = file; 115 | this.hash = hash; 116 | this.ext = ext; 117 | this.index = index; 118 | } 119 | 120 | public File rename(String newName) { 121 | file = Utils.rename(file, newName); 122 | return file; 123 | } 124 | 125 | //getters & setters 126 | public File getFile() { 127 | return file; 128 | } 129 | 130 | public void setFile(File file) { 131 | this.file = file; 132 | } 133 | 134 | public byte[] getHash() { 135 | return hash; 136 | } 137 | 138 | public void setHash(byte[] hash) { 139 | this.hash = hash; 140 | } 141 | 142 | public int getExt() { 143 | return ext; 144 | } 145 | 146 | public void setExt(int ext) { 147 | this.ext = ext; 148 | } 149 | 150 | public int getIndex() { 151 | return index; 152 | } 153 | 154 | public void setIndex(int index) { 155 | this.index = index; 156 | } 157 | } 158 | 159 | protected File src, output; 160 | protected List files; 161 | 162 | public Pck(File src, File output) { 163 | this.src = src; 164 | this.output = output; 165 | this.files = new ArrayList<>(); 166 | } 167 | 168 | public Pck(Pck pck) { 169 | this.src = pck.getSrc(); 170 | this.output = pck.getOutput(); 171 | this.files = pck.getFiles(); 172 | } 173 | 174 | //methods 175 | public synchronized void generateHeader() { 176 | try{ 177 | FileOutputStream _header = new FileOutputStream(new File(output, "_header")); 178 | JSONObject json = new JSONObject(); 179 | for(PckFile pckFile : getFiles()) { 180 | JSONObject sJson = new JSONObject(); 181 | sJson.put("hash", Utils.bytesToHex(pckFile.getHash())); 182 | sJson.put("file", pckFile.getFile().getName()); 183 | json.put(String.valueOf(pckFile.getIndex()), sJson); 184 | } 185 | _header.write(json.toString(4).getBytes()); 186 | _header.close(); 187 | Log.d("mJson", json.toString()); 188 | }catch(Exception e) { 189 | e.printStackTrace(); 190 | } 191 | } 192 | 193 | //getters & setters 194 | public File getSrc() { 195 | return src; 196 | } 197 | 198 | public File getOutput() { 199 | return output; 200 | } 201 | 202 | public void setOutput(File output) { 203 | this.output = output; 204 | for(PckFile pckFile : files) { 205 | pckFile.setFile(new File(output, pckFile.getFile().getName())); 206 | } 207 | } 208 | 209 | public void addFile(File file, byte[] hash, int ext, int index) { 210 | files.add(index, new PckFile(file, hash, ext, index)); 211 | } 212 | 213 | public PckFile getFile(int index) { 214 | for(PckFile pckFile : files) { 215 | if(pckFile.getIndex() == index) { 216 | return pckFile; 217 | } 218 | } 219 | return null; 220 | } 221 | 222 | public PckFile getFile(byte[] hash) { 223 | for(PckFile pckFile : files) { 224 | if(Arrays.equals(pckFile.getHash(), hash)) { 225 | return pckFile; 226 | } 227 | } 228 | return null; 229 | } 230 | 231 | public List getFiles(byte[] hash_part, int extId) { 232 | List matchingFiles = new ArrayList<>(); 233 | for(PckFile pckFile : getFiles(hash_part)) { 234 | if(pckFile.getExt() == extId) { 235 | matchingFiles.add(pckFile); 236 | } 237 | } 238 | return matchingFiles; 239 | } 240 | 241 | public List getFiles(byte[] hash_part) { 242 | List matchingFiles = new ArrayList<>(); 243 | byte[] hash_useful; 244 | for(PckFile pckFile : files) { 245 | hash_useful = Arrays.copyOf(pckFile.getHash(), 4); 246 | if(Arrays.equals(hash_part, hash_useful)) { 247 | matchingFiles.add(pckFile); 248 | } 249 | } 250 | return matchingFiles; 251 | } 252 | 253 | public List getFiles(int extId) { 254 | List matchingFiles = new ArrayList<>(); 255 | for(PckFile pckFile : files) { 256 | if(pckFile.getExt() == extId) { 257 | matchingFiles.add(pckFile); 258 | } 259 | } 260 | return matchingFiles; 261 | } 262 | 263 | public List getFiles() { 264 | return files; 265 | } 266 | } 267 | --------------------------------------------------------------------------------