Compare commits

...

4 commits

Author SHA1 Message Date
maelstrom 8a7b95beee More build system rework and moving stuff to patches 2024-08-17 21:25:06 +02:00
Your Name f2ada0e2fe Barebones patching system 2024-08-17 19:38:31 +02:00
Your Name e8ffddf93f so much more shit man 2024-08-17 19:14:45 +02:00
maelstrom 9e065cd638 Process of rewriting build system... yet again 2024-08-17 17:52:49 +02:00
23 changed files with 324 additions and 17899 deletions

9
.gitignore vendored
View file

@ -1,11 +1,8 @@
build/
source-apk/*.apk
keystores/*.keystore
tools/*
!tools/smali-2.5.2.jar
!tools/apktool_2.9.3.jar
!tools/apksigner.jar
!tools/d8.jar
.idea
*.iml
*.iml
__pycache__

15
.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,15 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python Debugger: Current File",
"type": "debugpy",
"request": "launch",
"program": "test.py",
"console": "integratedTerminal"
}
]
}

2
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,2 @@
{
}

234
build.py
View file

@ -1,234 +0,0 @@
import sys
import shutil
import os
from pathlib import Path
import subprocess
class BuildError(Exception):
def __init__(self):
super.__init__()
def getDexName(smaliName):
if smaliName == 'smali':
return 'classes.dex'
elif smaliName.startswith('smali_classes'):
return 'classes' + smaliName[13:] + '.dex'
print("Not a smali name")
raise BuildError()
class Task:
def clean():
shutil.rmtree('build')
def extractApk():
if Path('build/extracted').exists():
return
Path('build').mkdir(parents=True, exist_ok=True)
apk = list(filter(lambda f: f.endswith('.apk'), os.listdir('source-apk')))
if len(apk) == 0:
print('Please place the APK inside the source-apk directory.')
raise BuildError()
elif len(apk) > 1:
print('More than one apk found in source-apk. Please remove all but one.')
raise BuildError()
# Extract the apk
print("Extracting APK...")
subprocess.run(['java', '-jar', './tools/apktool_2.9.3.jar', 'd', 'source-apk/' + apk[0], '-o', 'build/extracted'])
def copySmali():
Task.extractApk()
Path('build/smali').mkdir(parents=True, exist_ok=True)
# Copy smali classes that haven't been yet
for file in os.listdir('build/extracted'):
if file != 'smali' and not file.startswith('smali_classes'):
continue
# Skip if already exists
if Path(f'build/smali/{file}').exists():
continue
print(f'Copying {file}...')
shutil.copytree(f'build/extracted/{file}', f'build/smali/{file}')
# Copy from src
print('Copying ykit smali...')
shutil.copytree('src/smali', 'build/smali', dirs_exist_ok=True)
def compileSmali():
Task.copySmali()
Path('build/dex').mkdir(parents=True, exist_ok=True)
def shouldCompileSmali(file, dex):
src = f'build/smali/{file}'
ykitsrc = f'src/smali/{file}'
dst = f'build/dex/{dex}'
# If dest doesn't exist, always build
if not Path(dst).exists():
return True
# Check if ykit src dir exists for it, if not then don't recompile
if not Path(ykitsrc).exists():
return False
# Get latest file timestamp in ykitsrc
files = list(Path(ykitsrc).rglob('*.smali'))
files = [os.path.getmtime(x) for x in files]
latest = max(files)
dsttime = os.path.getmtime(dst)
# If it is newer than the dest, then do recompile
if latest > dsttime:
return True
# Otherwise, don't
return False
for file in os.listdir('build/smali'):
dex = getDexName(file)
if not shouldCompileSmali(file, dex):
print(f"Skipping {file}...")
continue
print(f"Compiling {file}...")
subprocess.run(['java', '-jar', 'tools/smali-2.5.2.jar', 'a', f'build/smali/{file}', '-o', f'build/dex/{dex}'])
def genClassPath():
print("Generating java classpath from dex files...")
for file in os.listdir('build/dex'):
src = f'build/dex/{file}'
dst = f'build/jar/{file[:-4]}.jar'
srctime = os.path.getmtime(src)
dsttime = 0 if not Path(dst).exists() else os.path.getmtime(dst)
if srctime < dsttime:
print(f"Skipping {file}...")
continue
print(f"Dex2jar {file}...")
subprocess.run(['java', '-cp', 'tools/d2j/*', 'com.googlecode.dex2jar.tools.Dex2jarCmd', '-f', src, '-o', dst])
def compileJava():
Task.genClassPath()
sep = ';' if os.name == 'nt' else ':'
javaFiles = [str(x) for x in Path('src/java').rglob('*.java')]
classPath = sep.join([str(x) for x in Path('build/jar').rglob('*.jar')])
# Get latest java file timestamp
files = list(Path('src/java').rglob('*.java'))
files = [os.path.getmtime(x) for x in files]
latest = max(files)
# Get latest dex time or 0
latest2 = 0 if not Path('build/java_dex/classes.dex').exists() else os.path.getmtime('build/java_dex/classes.dex')
# If dex file is newer than source, no need to do anything
if latest2 > latest:
print("Skipping java...")
return
print("Compiling java...")
subprocess.run(['javac', '-cp', classPath, '-d', 'build/java_class', *javaFiles])
classFiles = [str(x) for x in Path('build/java_class').rglob('*.class')]
Path('build/java_dex').mkdir(parents=True, exist_ok=True)
subprocess.run(['java', '-cp', 'tools/d8.jar', 'com.android.tools.r8.D8', *classFiles, '--output', 'build/java_dex'])
def mergeResources():
Task.compileSmali()
Task.compileJava()
# Copy original resources
# on first run only
if not Path('build/merged').exists():
print('Copying extracted resources...')
shutil.copytree('build/extracted', 'build/merged', dirs_exist_ok=True, ignore=shutil.ignore_patterns('smali', 'smali_classes*'))
print("Copying ykit resources...")
shutil.copytree('src/resources', 'build/merged', dirs_exist_ok=True)
print("Copying compiled dex files...")
shutil.copytree('build/dex', 'build/merged', dirs_exist_ok=True)
shutil.copy('build/java_dex/classes.dex', 'build/merged/classes7.dex') # TODO: Unhardcode this
def buildApk():
Task.mergeResources()
debugFlag = ('-d',) if ('d' in ''.join(sys.argv[2:])) else ()
print("Building apk...")
subprocess.run(['java', '-jar', './tools/apktool_2.9.3.jar', 'b', *debugFlag, 'build/merged', '-o', 'build/apk/tumblr-ykit-unsigned.apk'])
def alignApk():
Task.buildApk()
print('Aligning apk...')
subprocess.run(['tools/zipalign', '-p', '-f', '-v', '4', 'build/apk/tumblr-ykit-unsigned.apk', 'build/apk/tumblr-ykit.apk'])
def debugKey():
if Path('keystores/debug.keystore').exists():
return
print("Debug keystore does not exist, generating...")
subprocess.run(['keytool', '-genkey', '-v', '-keystore', 'keystores/debug.keystore', '-alias', 'alias_name', '-keyalg', 'RSA', '-keysize', '2048', '-validity', '10000', '-storepass', '123456', '-keypass', '123456', '-dname', 'CN=, OU=, O=, L=, ST=, C='])
def signApk():
Task.alignApk()
Task.debugKey()
debugFlag = 'd' in ''.join(sys.argv[2:])
releaseFlag = 'r' in ''.join(sys.argv[2:])
if not (debugFlag or releaseFlag):
print("Neither debug nor release flag was specified so the apk will not be signed.")
return
if releaseFlag:
if not Path('keystores/release.keystore').exists():
print("release.keystore is missing from keystores directory")
return
keystoreOpts = ('--ks', 'keystores/release.keystore',)
else:
Task.debugKey()
keystoreOpts = ('--ks', 'keystores/debug.keystore', '--ks-pass', 'pass:123456',)
print('Signing apk...')
subprocess.run(['java', '-jar', './tools/apksigner.jar', 'sign', *keystoreOpts, 'build/apk/tumblr-ykit.apk'])
print("Successfully signed apk in 'build/apk/tumblr-ykit.apk' using " + ('release' if releaseFlag else 'debug') + ' keystore')
def deployToAndroidStudio():
projDir = Path.home() / 'ApkProjects/tumblr-ykit'
if not projDir.exists():
print("No project named 'tumblr-ykit' in ~/ApkProjects/")
shutil.copy('build/apk/tumblr-ykit.apk', projDir / 'tumblr-ykit.apk')
print("Deployed to Android Studio")
def main():
match sys.argv[1]:
case 'clean':
Task.clean()
case 'extract':
Task.extractApk()
case 'assemble':
Task.buildApk()
case 'build':
Task.signApk()
if ('a' in ''.join(sys.argv[2:])):
Task.deployToAndroidStudio()
case 'genclasspath':
Task.genClassPath()
if __name__ == '__main__':
main()

View file

@ -1 +1 @@
python build.py "$@"
python buildtool/build.py "$@"

5
buildtool/build.py Normal file
View file

@ -0,0 +1,5 @@
# from task.extract import extract
from task.patch_resources import patch_resources
if __name__ == '__main__':
_ = patch_resources()

View file

@ -0,0 +1,44 @@
start: (patch)*
patch: file_declaration instruction*
file_declaration: "file" string
?string: STRING | RAW_STRING | LONG_STRING
?instruction: insert
| preg_replace
| define_anchor
insert: "insert" location string
preg_replace: "preg_replace" range PATTERN string
define_anchor: "define_anchor" anchor affix PATTERN
| "define_anchor" range_anchor "range" PATTERN
?affix: "before" | "after"
?location: LINE_NUMBER
| CHAR_INDEX
| LINE_COLUMN
| anchor
range: "(" location "-" location ")"
| range_anchor
anchor: "@" IDENTIFIER
range_anchor: "@$" IDENTIFIER
STRING: /"([^"\\]|\\.)*"/
RAW_STRING: /'[^']*'/
LONG_STRING: /<<\s*(?P<terminator>[^\n]+)\n.*\n(?P=terminator)/s
LINE_NUMBER: /ln\d+/
CHAR_INDEX: /ch\d+/
LINE_COLUMN: /ln\d+c\d+/
IDENTIFIER: /[a-zA-Z_][a-zA-Z0-9_]*/
PATTERN: /\/([^\/\\]|\\.)*\//
COMMENT: /#[^\r\n]+/
%ignore /[\t \f\n]+/
%ignore COMMENT

63
buildtool/polly/parser.py Normal file
View file

@ -0,0 +1,63 @@
from lark import Lark
from lark.lexer import Token
from lark.tree import Branch
import os
import re
def parse_string(token: Token):
if token.type == 'STRING':
string = token.value[1:-1]
string = re.sub(r"\\n","\n", string)
string = re.sub(r"\\t","\t", string)
string = re.sub(r"\\r","\r", string)
string = re.sub(r"\\(.)",r"\1", string)
return string
elif token.type == 'RAW_STRING':
string = token.value[1:-1]
return string
elif token.type == 'LONG_STRING':
string = re.match(re.compile(r"<<\s*(?P<terminator>[^\n]+)\n(.*)\n(?P=terminator)", re.MULTILINE + re.DOTALL),token.value)
assert string is not None
string = string.group(2)
return string
def parse_location(location: Token):
assert isinstance(location.value, str)
m = re.match(r"ln([0-9]+)(?:c([0-9]+))?", location.value)
if m:
ln, col = m.groups()
return {"type": "lncol", "line": int(ln), "column": int(col) if col else 0}
m = re.match(r"ch([0-9]+)", location.value)
if m:
ch = m.groups()[0]
return {"type": "char", "index": int(ch)}
raise RuntimeError("Cannot parse location")
def parse_patch(branch: Branch[Token], mtime: float):
# First instruction is always file declaration
target_file = parse_string(branch.children[0].children[0]) # pyright: ignore[reportUnknownMemberType, reportArgumentType]
actions = []
for inst in branch.children[1:]:
match inst.data:
case "insert":
actions.append({"type": "insert", "at": parse_location(inst.children[0]), "content": parse_string(inst.children[1])})
# print(f"Inserting {parse_string(inst.children[1])} at {inst.children[0]} in {target_file}")
return {"target": target_file, "actions": actions, "timestamp": mtime}
def parse_patch_file(file: str):
lark = Lark.open('grammar.lark', rel_to=__file__)
mtime = os.path.getmtime(file)
with open(file, 'r') as f:
result = lark.parse(f.read())
patches = [parse_patch(patch, mtime) for patch in result.children]
return patches

View file

@ -0,0 +1,71 @@
from .parser import parse_patch_file
from pathlib import Path
from task.util import *
def resolve_position(location, src: str):
if location["type"] == "char":
return location["index"]
elif location["type"] == "lncol":
j = 0
for i, x in enumerate(src.split('\n')):
if (i+1) == location["line"]:
if location["column"] > len(x):
print(f"Cannot insert at line {location['line']} column {location['column']}. The line is not that long.")
exit(-1)
return j + location["column"]
j += len(x) + 1
print(f"Cannot insert at line {location['line']}. The file is not long enough.")
exit(-1)
def read_insertion(action, src):
position = resolve_position(action["at"], src)
return {"content": action["content"], "pos": position}
def process_patch_file(patch_file: str, check_date: bool = True):
patches = parse_patch_file(patch_file)
for patch in patches:
target_src = EXTRACTED_DIR / patch["target"]
target_dest = PATCHED_RSC_DIR / patch["target"]
# Skip old patches
if check_date and target_dest.exists() and target_dest.stat().st_mtime > patch["timestamp"]:
print(f"Skipping patch {patch['target']}...")
continue
print(f"Patching {patch['target']} with {len(patch['actions'])} actions")
# Read the source of the file
with open(target_src, 'r') as f:
src = f.read()
# Resolve the actual char locations in the patches
insertions = [read_insertion(action, src) for action in patch["actions"]]
insertions = [x for x in insertions if x is not None]
for insertion in insertions:
src = src[:insertion["pos"]] + insertion["content"] + src[insertion["pos"]:]
# Correct insertions that follow this one
for i2 in insertions:
# Only update insertions that come *after* this one
if i2 == insertion or i2["pos"] < insertion["pos"]:
continue
i2["pos"] += len(insertion["content"])
# Write the changed output
with open(target_dest, 'w') as f:
f.write(src)
def process_all_patches(dir: Path | str, check_date: bool = True):
dir = Path(dir)
for (cd, _, files) in dir.walk():
for f in files:
if f.endswith(".patch"):
print("Processing patch {}".format((cd / f).relative_to(dir)))
process_patch_file(str(cd / f), check_date=check_date)

25
buildtool/task/extract.py Normal file
View file

@ -0,0 +1,25 @@
from .util import *
import subprocess
def find_apk():
apks = list(SOURCE_APK_DIR.glob("*.apk"))
if len(apks) == 0:
print("Source APK is missing from 'source-apk' directory. Please make sure to copy it to the right directory.")
exit(-1)
elif len(apks) > 1:
print("Multiple APKs were found in the 'source-apk' directory. Please only place the correct APK there.")
exit(-1)
return apks[0]
def extract():
target_apk = find_apk()
# Check if this task is necessary
if EXTRACTED_DIR.exists() and EXTRACTED_DIR.stat().st_mtime > target_apk.stat().st_mtime:
return "pass"
# Extract the apk
print("Extracting APK...")
_ = subprocess.run(['java', '-jar', './tools/apktool_2.9.3.jar', 'd', str(target_apk.absolute()), '-o', EXTRACTED_DIR])

View file

@ -0,0 +1,23 @@
from pathlib import Path
import shutil
def merge_into(src: Path | str, dest: Path | str, check_date: bool = True):
src = Path(src)
dest = Path(dest)
changed = False
for (cd, _, files) in src.walk():
for file in files:
src_file = cd / file
dest_file = dest / src_file.relative_to(src)
# Don't update if dest is newer than source
if check_date and dest_file.exists() and src_file.stat().st_mtime < dest_file.stat().st_mtime:
continue
dest_file.parent.mkdir(parents=True, exist_ok=True)
_ = shutil.copyfile(src_file, dest_file)
changed = True
return changed

View file

@ -0,0 +1,29 @@
from .util import *
from . import fileutil
import shutil
from polly.patcher import process_all_patches
# Task dependencies
from .extract import extract
def patch_resources():
_ = extract()
first_time = False
# Copy original resources only the first time
if not PATCHED_RSC_DIR.exists():
print("Copying original resources...")
shutil.copytree(EXTRACTED_DIR, PATCHED_RSC_DIR, ignore=shutil.ignore_patterns("smali*"))
first_time = True
s = fileutil.merge_into(SRC_RESOURCES_DIR, PATCHED_RSC_DIR, check_date=not first_time)
if s:
print("Copied custom resources from src/resources")
else:
print("Skipped copying resources from src/resources")
# Patch other resources
process_all_patches(SRC_PATCHES_DIR, check_date=not first_time)

11
buildtool/task/util.py Normal file
View file

@ -0,0 +1,11 @@
from pathlib import Path
SOURCE_APK_DIR = Path("source-apk")
BUILD_DIR = Path("build")
EXTRACTED_DIR = BUILD_DIR / "extracted"
PATCHED_RSC_DIR = BUILD_DIR / "patched_resources"
SRC_DIR = Path("src")
SRC_RESOURCES_DIR = SRC_DIR / "resources"
SRC_PATCHES_DIR = SRC_DIR / "patches"

1
msys.bat Normal file
View file

@ -0,0 +1 @@
C:\msys64\msys2_shell.cmd -here -defterm -no-start -full-path

16
src/patches/public.patch Normal file
View file

@ -0,0 +1,16 @@
file 'res/values/public.xml'
insert ln17595 << @end
<!-- Ykit -->
<public type="drawable" name="oval_yellow" id="0x7f08f001" /> <!-- YKit-BringBackPhoebe -->
<public type="id" name="ykit_settings" id="0x7f0bf001" /> <!-- YKit-Settings -->
<public type="id" name="ykit_version" id="0x7f0bf003" /> <!-- YKit-Settings -->
<public type="id" name="text_color_yellow" id="0x7f0bf002" /> <!-- YKit-BringBackPhoebe -->
<public type="layout" name="activity_ykit_settings" id="0x7f0ef001" /> <!-- YKit-Settings -->
<public type="raw" name="ykit_meow" id="0x7f12f001" /> <!-- YKit-Settings -->
@end
#17595

6
src/patches/yellow.patch Normal file
View file

@ -0,0 +1,6 @@
file 'res/layout/color_options_toolbar.xml'
insert ln8 << @end
<androidx.appcompat.widget.AppCompatImageButton android:id="@id/text_color_yellow" android:background="?selectableItemBackgroundBorderless" android:layout_width="48.0dip" android:layout_height="48.0dip" android:layout_marginLeft="2.0dip" android:layout_marginRight="2.0dip" android:layout_weight="0.5" app:srcCompat="@drawable/oval_yellow" />
@end

View file

@ -0,0 +1,6 @@
file 'res/layout/fragment_blog_settings.xml'
insert ln10 << @end
<com.tumblr.ui.widget.TMBlogSettingsTextRow android:id="@id/ykit_settings" tumblr:listItemDetail="Change settings for YKit" tumblr:listItemTitle="YKit Settings" style="@style/DetailedListItem" />
@end

View file

@ -1,14 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<merge android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="48.0dip"
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto">
<LinearLayout android:gravity="center" android:orientation="horizontal" android:id="@id/color_editing_controls" android:background="?selectableItemBackgroundBorderless" android:layout_width="fill_parent" android:layout_height="48.0dip">
<androidx.appcompat.widget.AppCompatImageButton android:id="@id/text_color_default" android:background="?selectableItemBackgroundBorderless" android:layout_width="48.0dip" android:layout_height="48.0dip" android:layout_marginLeft="2.0dip" android:layout_marginRight="2.0dip" android:layout_weight="0.5" app:srcCompat="@drawable/oval_main_text_color" />
<androidx.appcompat.widget.AppCompatImageButton android:id="@id/text_color_red" android:background="?selectableItemBackgroundBorderless" android:layout_width="48.0dip" android:layout_height="48.0dip" android:layout_marginLeft="2.0dip" android:layout_marginRight="2.0dip" android:layout_weight="0.5" app:srcCompat="@drawable/oval_red" />
<androidx.appcompat.widget.AppCompatImageButton android:id="@id/text_color_orange" android:background="?selectableItemBackgroundBorderless" android:layout_width="48.0dip" android:layout_height="48.0dip" android:layout_marginLeft="2.0dip" android:layout_marginRight="2.0dip" android:layout_weight="0.5" app:srcCompat="@drawable/oval_orange" />
<androidx.appcompat.widget.AppCompatImageButton android:id="@id/text_color_yellow" android:background="?selectableItemBackgroundBorderless" android:layout_width="48.0dip" android:layout_height="48.0dip" android:layout_marginLeft="2.0dip" android:layout_marginRight="2.0dip" android:layout_weight="0.5" app:srcCompat="@drawable/oval_yellow" />
<androidx.appcompat.widget.AppCompatImageButton android:id="@id/text_color_green" android:background="?selectableItemBackgroundBorderless" android:layout_width="48.0dip" android:layout_height="48.0dip" android:layout_marginLeft="2.0dip" android:layout_marginRight="2.0dip" android:layout_weight="0.5" app:srcCompat="@drawable/oval_green" />
<androidx.appcompat.widget.AppCompatImageButton android:id="@id/text_color_blue" android:background="?selectableItemBackgroundBorderless" android:layout_width="48.0dip" android:layout_height="48.0dip" android:layout_marginLeft="2.0dip" android:layout_marginRight="2.0dip" android:layout_weight="0.5" app:srcCompat="@drawable/oval_blue" />
<androidx.appcompat.widget.AppCompatImageButton android:id="@id/text_color_purple" android:background="?selectableItemBackgroundBorderless" android:layout_width="48.0dip" android:layout_height="48.0dip" android:layout_marginLeft="2.0dip" android:layout_marginRight="2.0dip" android:layout_weight="0.5" app:srcCompat="@drawable/oval_purple" />
<androidx.appcompat.widget.AppCompatImageButton android:id="@id/text_color_pink" android:background="?selectableItemBackgroundBorderless" android:layout_width="48.0dip" android:layout_height="48.0dip" android:layout_marginLeft="2.0dip" android:layout_marginRight="2.0dip" android:layout_weight="0.5" app:srcCompat="@drawable/oval_pink" />
</LinearLayout>
</merge>

View file

@ -1,43 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<com.tumblr.ui.widget.fab.ObservableScrollView android:id="@id/blog_settings_scroll_view" android:background="?themeHighlightedContentBackgroundColor" android:paddingTop="@dimen/action_bar_base_height" android:layout_width="fill_parent" android:layout_height="fill_parent"
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tumblr="http://schemas.android.com/apk/res-auto">
<LinearLayout android:background="@null" style="@style/BlogSettingsContainer">
<LinearLayout android:id="@id/user_blog_default_options" style="@style/BlogSettingsUserBlogOptionsContainer">
<com.tumblr.ui.widget.TMBlogSettingsTextRow android:id="@id/account_settings" tumblr:listItemDetail="@string/global_settings_subtitle" tumblr:listItemTitle="@string/account_settings" style="@style/DetailedListItem" />
<com.tumblr.ui.widget.TMBlogSettingsTextRow android:id="@id/blog_ad_free_management" android:visibility="gone" tumblr:listItemDetail="@string/tumblr_premium_subtitle" tumblr:listItemTitle="@string/tumblr_premium" style="@style/DetailedListItem" />
<com.tumblr.ui.widget.TMBlogSettingsTextRow android:id="@id/blog_payment_and_purchases" tumblr:listItemDetail="@string/payment_and_purchases_desc" tumblr:listItemTitle="@string/payment_and_purchases" style="@style/DetailedListItem" />
<com.tumblr.ui.widget.TMBlogSettingsTextRow android:id="@id/blog_subscriptions_and_purchases" tumblr:listItemDetail="@string/subscriptions_and_purchases_desc" tumblr:listItemTitle="@string/subscriptions_and_purchases" style="@style/DetailedListItem" />
<com.tumblr.ui.widget.TMBlogSettingsTextRow android:id="@id/ykit_settings" tumblr:listItemDetail="Change settings for YKit" tumblr:listItemTitle="YKit Settings" style="@style/DetailedListItem" />
</LinearLayout>
<LinearLayout style="@style/BlogSettingsOptionsSection">
<TextView android:id="@id/blog_settings_header" android:background="?themeHighlightedContentBackgroundColor" style="@style/SettingsSectionHeader" />
<com.tumblr.ui.widget.UserBlogOptionsLayout android:id="@id/user_blog_options_container" style="@style/BlogSettingsUserBlogOptionsContainer">
<com.tumblr.ui.widget.TMBlogSettingsTextRow android:id="@id/blog_change_username_row" android:visibility="gone" tumblr:listItemTitle="@string/change_name_title" style="@style/BlogSettingsListItem" />
<com.tumblr.ui.widget.TMBlogSettingsTextRow android:id="@id/blog_pages_row" android:visibility="gone" tumblr:listItemDetail="@string/pages_description" tumblr:listItemTitle="@string/pages" style="@style/DetailedListItem.White" />
<com.tumblr.ui.widget.TMBlogSettingsTextRow android:id="@id/blog_featured_tags" android:visibility="gone" tumblr:listItemTitle="@string/featured_tags" style="@style/BlogSettingsListItem" />
<com.tumblr.ui.widget.TMBlogSettingsTextRow android:id="@id/blog_followers_row" tumblr:listItemTitle="@string/followers_title" style="@style/BlogSettingsListItem" />
<com.tumblr.ui.widget.TMBlogSettingsTextRow android:id="@id/blog_drafts_row" tumblr:listItemTitle="@string/drafts_title" style="@style/BlogSettingsListItem" />
<com.tumblr.ui.widget.TMBlogSettingsTextRow android:id="@id/blog_queue_row" tumblr:listItemTitle="@string/queue_title" style="@style/BlogSettingsListItem" />
<com.tumblr.ui.widget.TMBlogSettingsTextRow android:id="@id/blog_inbox_row" tumblr:listItemDetail="@string/inbox_detail_new" tumblr:listItemTitle="@string/inbox_title" style="@style/DetailedListItem" />
<com.tumblr.ui.widget.TMBlogSettingsTextRow android:id="@id/blog_community_label_row" android:visibility="gone" tumblr:listItemTitle="@string/blog_community_label_settings" style="@style/BlogSettingsListItem" />
<com.tumblr.ui.widget.TMBlogSettingsTextRow android:id="@id/blog_posts_review_row" android:visibility="gone" tumblr:listItemTitle="@string/posts_review_setting_label" style="@style/BlogSettingsListItem" />
<com.tumblr.ui.widget.TMBlogSettingsTextRow android:id="@id/blaze_info_page" android:visibility="gone" tumblr:listItemTitle="@string/tumblr_blaze" style="@style/BlogSettingsListItem" />
<com.tumblr.ui.widget.TMBlogSettingsTextRow android:id="@id/manage_gifts" android:visibility="gone" tumblr:listItemTitle="@string/gifts" style="@style/BlogSettingsListItem" />
<com.tumblr.ui.widget.TMBlogSettingsTextRow android:id="@id/blog_payouts" android:visibility="gone" tumblr:listItemTitle="@string/payouts_settings" style="@style/BlogSettingsListItem" />
<com.tumblr.ui.widget.TMBlogSettingsTextRow android:id="@id/blog_privacy_row" tumblr:listItemTitle="@string/setting_label_blog_privacy_title" style="@style/BlogSettingsListItem" />
<com.tumblr.ui.widget.TMBlogSettingsTextRow android:id="@id/blog_delete_row" tumblr:listItemTitle="@string/delete_blog" style="@style/BlogSettingsListItem" />
<com.tumblr.ui.widget.TMToggleWithWarningRow android:id="@id/blog_allow_tipping" android:paddingStart="6.0dip" tumblr:rowText="@string/allow_tipping" tumblr:toggleDefault="false" style="@style/SettingsToggleSettingRow" />
<com.tumblr.ui.widget.TMToggleWithWarningRow android:id="@id/blog_allow_blaze_by_others" android:paddingStart="6.0dip" tumblr:rowText="@string/allow_blaze_by_others_v2" tumblr:toggleDefault="false" style="@style/SettingsToggleSettingRow" />
<com.tumblr.ui.widget.TMToggleRow android:id="@id/blog_show_tip_button" android:paddingStart="6.0dip" tumblr:rowText="@string/tipping_tip_blog" tumblr:toggleDefault="false" style="@style/SettingsToggleSettingRow" />
<com.tumblr.ui.widget.TMToggleRow android:id="@id/blog_post_tipping_default" android:paddingStart="6.0dip" tumblr:rowText="@string/tipping_post_default" tumblr:toggleDefault="false" style="@style/SettingsToggleSettingRow" />
<com.tumblr.ui.widget.TMToggleRow android:id="@id/blog_allow_submissions" android:paddingStart="6.0dip" tumblr:rowText="@string/blog_allow_submissions" tumblr:toggleDefault="true" style="@style/SettingsToggleSettingRow" />
<com.tumblr.ui.widget.TMToggleRow android:id="@id/blog_allow_asks" android:paddingStart="6.0dip" tumblr:rowText="@string/blog_allow_asks" tumblr:toggleDefault="true" style="@style/SettingsToggleSettingRow" />
<com.tumblr.ui.widget.TMToggleRow android:id="@id/blog_ask_allow_anonymous" android:paddingStart="6.0dip" tumblr:rowText="@string/blog_ask_allow_anonymous" tumblr:toggleDefault="true" style="@style/SettingsToggleSettingRow" />
<com.tumblr.ui.widget.TMToggleRow android:id="@id/blog_ask_allow_media" android:paddingStart="6.0dip" tumblr:rowText="@string/blog_ask_allow_with_media" tumblr:toggleDefault="true" style="@style/SettingsToggleSettingRow" />
<com.tumblr.ui.widget.TMBlogSettingsTextRow android:id="@id/blog_ask_page_title" android:minHeight="0.0dip" tumblr:listItemTitle="@string/blog_ask_page_title" style="@style/BlogSettingsListItem" />
<com.tumblr.ui.widget.TMToggleRow android:id="@id/blog_show_top_posts" android:paddingStart="6.0dip" tumblr:rowText="@string/blog_show_top_posts" tumblr:toggleDefault="true" style="@style/SettingsToggleSettingRow" />
<com.tumblr.ui.widget.TMBlogSettingsTextRow android:id="@id/blog_blocked_tumblrs_row" tumblr:listItemTitle="@string/blocked_tumblrs_title" style="@style/BlogSettingsListItem" />
</com.tumblr.ui.widget.UserBlogOptionsLayout>
</LinearLayout>
</LinearLayout>
</com.tumblr.ui.widget.fab.ObservableScrollView>

File diff suppressed because it is too large Load diff

3
test.py Normal file
View file

@ -0,0 +1,3 @@
from buildtool.polly.patcher import process_patch_file
process_patch_file('src/patches/public.patch', check_date=True)

Binary file not shown.

Binary file not shown.