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 |
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 |
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 |
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 | 
37 |
38 | 
39 |
40 | 
41 |
42 | 
43 |
44 | 
45 |
46 | 
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 |
95 |
96 |
97 |
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 |
101 |
102 |
103 |
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 |
88 |
89 | )}
90 | {activeListInfoOpen &&
91 |
92 |
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 |
174 | {/* {}} /> */}
175 |
176 |
177 |
178 | {/* */}
179 |
180 | {/* */}
181 |
182 | {drawerOpen && setDrawerOpen(false)} />}
183 |
184 |
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 |
--------------------------------------------------------------------------------