Compare commits

...

42 Commits

Author SHA1 Message Date
Trevor 6fa97a21f6 merge master 2025-10-01 13:34:16 -05:00
Trevor b71d1d3cd6 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-10-01 13:27:51 -05:00
Trevor 3ab38c7e4e changed from global to coroutine 2025-10-01 13:14:25 -05:00
Trevor 1abcc92a3b addressing pr comments 2025-10-01 13:11:59 -05:00
Trevor 25ab7aff92 Improve QR code error text styling to match app design
- Add proper margins (20dp start/end) for better spacing
- Center the text alignment for better visual balance
- Use full width (0dp) with constraints for consistent layout
- Match the styling patterns used elsewhere in the app
- Improves readability and visual consistency when QR code is too large
2025-08-20 11:01:30 -05:00
Trevor d8a0781d10 Fix export dialog to close instead of auto-sharing when clicked outside
- Change default close action from 0 (share) to -1 (no action)
- This prevents the dialog from automatically sharing when users click outside
- Users must now explicitly choose Share or Save to Device to proceed
- Improves user experience by preventing accidental sharing
2025-08-20 10:59:22 -05:00
Trevor 42040e5f3d Center the export dialog buttons
- Add center: true parameter to both Share and Save to Device Action constructors
- This centers the buttons horizontally in the dialog layout
- Buttons are now both equal size and properly centered for better visual balance
- Improves the overall UI appearance and user experience
2025-08-20 10:52:28 -05:00
Trevor f00e71522f Make export dialog buttons equal size
- Change both Share and Save to Device buttons to use ActionStyle.NONE
- This creates two equal-sized buttons side by side instead of primary/secondary styling
- Improves UI consistency and makes both options equally prominent
- Users can now easily choose between sharing or saving without visual hierarchy bias
2025-08-20 10:49:22 -05:00
Trevor 8916fd35ab Add save to device option for Polycentric profile export
- Add dialog with two options: Share or Save to Device
- Implement saveToDevice method using MediaStore API to save to Downloads folder
- Add necessary string resources for the new UI elements
- Use UIDialogs.showDialog with Action and ActionStyle for proper dialog handling
- Add ContentValues and MediaStore imports for file saving functionality

Users can now choose to either share the exported profile file or save it directly to their device's Downloads folder for later use.
2025-08-20 10:34:14 -05:00
Trevor cfb030f59b Remove debug logging from file sharing implementation
- Remove all debug logging added during troubleshooting
- Keep the essential fix for EXTRA_STREAM Parcelable handling
- Clean up the code while maintaining the working functionality
- Remove verbose logging from handleIntent, handleUrlAll, handleContent, and handleUnknownText

The file sharing functionality is now working correctly and the code is clean.
2025-08-19 11:59:08 -05:00
Trevor 71f626e8e0 Fix file sharing to properly handle EXTRA_STREAM Parcelable
- Add support for EXTRA_STREAM as Parcelable URI instead of just String
- Check for streamParcelable first, then fall back to string extras
- Add logging to show both String and Parcelable EXTRA_STREAM values
- This should fix the issue where file sharing was receiving description text
instead of actual file content

The logs showed that EXTRA_STREAM was null as String but the file content
should be available as a Parcelable URI.
2025-08-19 11:56:30 -05:00
Trevor 6f4d7fd6b9 Add comprehensive debugging for file sharing intent handling
- Add logging for all intent details (action, data, type, extras)
- Add detailed logging for ACTION_SEND intent processing
- Add logging for targetData processing and handleUrlAll calls
- Add logging for URI scheme detection
- This will help identify where the file sharing process is failing

The logs will show the complete flow from intent reception to processing.
2025-08-19 11:54:29 -05:00
Trevor f207ef9954 Add debugging logs for Polycentric file import
- Add logging to handleContent method to show file path and MIME type
- Add logging to show file content length and first 100 characters
- Add logging to handleUnknownText method to show received text content
- Add warning log when text format is not recognized
- This will help debug why Polycentric profile file import isn't working

The logs will show exactly what content is being received when sharing
Polycentric profile files to Grayjay.
2025-08-19 11:51:21 -05:00
Trevor 1ad3b7ae8f Fix Polycentric profile file import in MainActivity
- Update handleUnknownText method to detect Polycentric profile data
- Check for 'polycentric://' prefix in shared text files
- Launch PolycentricImportProfileActivity when Polycentric profile is detected
- Add logging for Polycentric profile detection
- Fixes 'unknown url format' error when sharing Polycentric profile files to Grayjay

This completes the file sharing workflow: users can now export Polycentric
profiles to files and share them to Grayjay, which will automatically
detect and import the profile data.
2025-08-19 11:48:18 -05:00
Trevor 217d738dd1 Add file import functionality to PolycentricImportProfileActivity
- Add 'Import from File' button to the import profile UI
- Add file picker launcher using ActivityResultContracts.GetContent()
- Support importing Polycentric profiles from text files
- Read file content and pass it to the existing import method
- Add string resource for 'Import from File' button text
- Update layout to include file import button between QR scan and manual entry

This allows users to import Polycentric profiles from files exported by
the backup functionality, completing the export/import workflow.
2025-08-19 11:44:01 -05:00
Trevor 799dad8875 Fix FileProvider authority to use package name directly
- Change FileProvider authority from string resource to use packageName directly
- Update AndroidManifest.xml to use ${applicationId}.fileprovider instead of @string/authority
- This ensures the authority matches the actual package name for each build variant
- Fixes the 'Couldn\'t find meta-data for provider' error in debug builds

The issue was that different build flavors have different package names
(com.futo.platformplayer.d for debug, com.futo.platformplayer for stable, etc.)
but the string resource wasn\'t being resolved correctly for all variants.
2025-08-19 11:36:53 -05:00
Trevor dc91987e88 updated file export 2025-08-19 11:23:56 -05:00
Trevor 8f5d90a1c8 Fix UninitializedPropertyAccessException in PolycentricBackupActivity
- Move export bundle creation before try-catch block to ensure _exportBundle is initialized
- Fix crash when accessing _exportBundle in error logging before it's been set
- Use local bundle variable in catch block instead of accessing uninitialized _exportBundle
- Ensure _exportBundle is always available for UI operations and error handling

This resolves the crash that occurred when QR code generation failed and the app
tried to access the uninitialized _exportBundle property.
2025-08-19 11:21:34 -05:00
Trevor 6e58107a5e Fix QR code size validation to use byte count instead of character count
- Replace character-based limit with byte-based limit using UTF-8 encoding
- QR codes are limited by bytes, not characters (1 char can be 1-4 bytes in UTF-8)
- Set limit to 2300 bytes (QR Code Version 40 with Error Correction Level M can hold ~2331 bytes)
- Update error logging to show both character count and byte size for debugging
- Apply fix to both PolycentricBackupActivity and QRCodeFullscreenActivity

This should now correctly handle strings with multi-byte characters and provide
accurate size validation for QR code generation.
2025-08-19 11:19:11 -05:00
Trevor 79f478e421 Fix QR code size limits and add better error logging
- Correct QR code capacity limits for Error Correction Level M (15%)
- Update limit from 2900 to 2300 characters (actual M level capacity is ~2334)
- Add detailed error logging to show bundle length when QR generation fails
- Fix FileProvider authority to use dynamic string resource instead of hardcoded value
- Ensure consistent limits across both PolycentricBackupActivity and QRCodeFullscreenActivity

The previous limit was based on Error Correction Level L (7%) but we use Level M (15%),
which has a lower capacity but better error recovery.
2025-08-19 11:13:45 -05:00
Trevor a20ebd49a4 Add proactive QR code size validation and checks
- Add isContentSuitableForQRCode() helper function with 2900 character limit
- Check QR code size before attempting generation in both activities
- Prevent fullscreen QR viewer from launching when data is too large
- Use conservative 2900 character limit (vs theoretical 2953 max)
- Add size validation in both PolycentricBackupActivity and QRCodeFullscreenActivity
- Improve user experience by failing fast with clear error messages

This ensures QR codes are only generated when they have a reasonable
chance of success, and prevents unnecessary attempts to generate
QR codes that are too large for reliable scanning.
2025-08-19 10:59:16 -05:00
Trevor 31f0109438 Fix TransactionTooLargeException in QRCodeFullscreenActivity
- Remove bitmap from Intent extras to prevent TransactionTooLargeException
- Pass only QR text through Intent and regenerate bitmap in fullscreen activity
- Add QR code generation logic to QRCodeFullscreenActivity
- Update createIntent method signature to only accept QR text
- Fixes crash when launching fullscreen QR viewer with large bitmaps

This resolves the 800KB+ data parcel size issue that was causing
the activity to fail to launch.
2025-08-19 10:29:16 -05:00
Trevor bbd9ba0a0a fixd import 2025-08-18 16:02:29 -05:00
Trevor bc67f4c486 Simplify Polycentric profile export with file export option
- Remove GZIP compression logic that was causing crashes
- Revert to simple QR code generation with single error correction level
- Add file export button when QR code is too large for scanning
- Add FileProvider configuration for sharing exported files
- Update string resources for new file export functionality
- Keep fullscreen QR code viewer for smaller profiles

This provides a more reliable solution: QR codes work for smaller profiles,
and file export handles large profiles that exceed QR code limits.
2025-08-18 14:26:25 -05:00
Trevor c862b60c71 Fix SettingsActivity crash by using local context instead of global context
- Replace StateApp.instance.initializeFiles() with direct FragmentedStorage initialization
- Use SettingsActivity's own filesDir instead of relying on global context
- Add FragmentedStorage import to resolve compilation errors
- Prevents 'Attempted to use a global context while MainActivity is no longer available' error

This ensures SettingsActivity can initialize files independently without
depending on MainActivity's global context state.
2025-08-13 12:16:26 -05:00
Trevor 1524687f75 Fix SettingsActivity crash when files directory not initialized
- Add StateApp.instance.initializeFiles() calls before accessing Settings
- Fixes crash when SettingsActivity is launched before MainActivity
- Ensures FragmentedStorage is properly initialized before loading files

This prevents the 'Files dir should be initialized before loading a file'
error that was causing the SettingsActivity to crash on startup.
2025-08-13 12:10:40 -05:00
Trevor cb74e82fa1 Add fullscreen QR code viewer for easier scanning
- Create QRCodeFullscreenActivity for large QR code display
- Add click listener to QR code image in PolycentricBackupActivity
- Add visual feedback with ripple effect and hint text
- Add localized strings for fullscreen hint
- Update layout to include hint text below QR code
- Add activity to AndroidManifest.xml

This makes it much easier to scan QR codes by providing
a fullscreen view when tapping the QR code image.
2025-08-13 11:59:56 -05:00
Trevor 4b3e89d0af Fix QR code generation for large polycentric export bundles
- Add GZIP compression for large export data (>2000 chars)
- Implement fallback QR generation with different error correction levels
- Add automatic decompression support in import functionality
- Improve error handling with fallback to text display
- Add localized error messages for QR code failures
- Add compression ratio logging for debugging

This fixes the 'Data too big' error when generating QR codes for
polycentric profile exports by automatically compressing large data
and providing multiple fallback mechanisms.
2025-08-13 11:34:56 -05:00
Trevor 31a34e4583 Merge branch 'master' into ts/polycentric-moderation 2025-08-01 10:14:16 -05:00
Trevor 19b96c2ea1 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-08-01 10:13:19 -05:00
Trevor 545ece59ad fix for new server backfill 2025-08-01 10:13:06 -05:00
Trevor c93fc664f6 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-07-08 12:45:10 -05:00
Koen J eb8e9270e0 Merge branch 'ts/polycentric-moderation' of gitlab.futo.org:videostreaming/grayjay into ts/polycentric-moderation 2025-07-08 16:30:49 +02:00
Koen J 7aea4a3f9f Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay into ts/polycentric-moderation 2025-07-08 16:30:18 +02:00
Trevor 76ce60d679 fixed comments not deleting in the video player 2025-07-02 14:40:11 -05:00
Trevor 24dbb543ae added blurb to explain moderation to moderation page 2025-07-02 13:06:30 -05:00
Trevor 79a2c6484c updated submodule 2025-07-02 12:09:41 -05:00
Trevor 3aa873f82b moderation does not apply to comments posted by user 2025-07-02 10:49:56 -05:00
Trevor 49f3fa9cb8 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-07-01 15:15:31 -05:00
Trevor 49367c6c75 added moderation button to polycentric screen and sliders for moderation 2025-04-21 14:37:27 -05:00
Trevor e1cb3308df pre-build 2025-04-08 17:46:43 -05:00
Trevor eb48d6c494 initial polycentric moderation implementation 2025-04-08 17:41:47 -05:00
50 changed files with 966 additions and 69 deletions
-3
View File
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ea10d3c5562c9f449a4e89e9c3dfcf881ed79a952f3409bc005bcc62c2cf4b81
size 65512557
+8
View File
@@ -238,5 +238,13 @@
android:name=".activities.SyncShowPairingCodeActivity"
android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity
android:name=".activities.PolycentricModerationActivity"
android:exported="false"
android:screenOrientation="portrait" />
<activity
android:name=".activities.QRCodeFullscreenActivity"
android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
</application>
</manifest>
@@ -0,0 +1,80 @@
package com.futo.platformplayer
import android.content.Context
import android.content.SharedPreferences
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import org.json.JSONObject
class ModerationsManager private constructor(context: Context) {
private val prefs: SharedPreferences = context.getSharedPreferences("polycentric_moderation", Context.MODE_PRIVATE)
private val _moderationLevels = MutableLiveData<Map<String, Int>>()
val moderationLevels: LiveData<Map<String, Int>> = _moderationLevels
init {
loadModerationLevels()
}
private fun loadModerationLevels() {
val levels = mutableMapOf<String, Int>()
levels["hate"] = prefs.getInt("offensive_level", 2)
levels["sexual"] = prefs.getInt("explicit_level", 1)
levels["violence"] = prefs.getInt("violence_level", 1)
_moderationLevels.value = levels
}
fun setModerationLevel(category: String, level: Int) {
when (category) {
"hate" -> prefs.edit().putInt("offensive_level", level).apply()
"sexual" -> prefs.edit().putInt("explicit_level", level).apply()
"violence" -> prefs.edit().putInt("violence_level", level).apply()
}
val currentMap = _moderationLevels.value?.toMutableMap() ?: mutableMapOf()
currentMap[category] = level
_moderationLevels.value = currentMap
}
fun getModerationLevelsJson(): String {
val json = JSONObject()
moderationLevels.value?.forEach { (key, value) ->
json.put(key, value)
}
return json.toString()
}
fun shouldFilter(category: String, contentLevel: Int): Boolean {
val userLevel = when (category) {
"hate" -> prefs.getInt("offensive_level", 2)
"sexual" -> prefs.getInt("explicit_level", 1)
"violence" -> prefs.getInt("violence_level", 1)
else -> 3
}
return contentLevel > userLevel
}
fun getCurrentModerationLevels(): Map<String, Int>? {
return moderationLevels.value
}
companion object {
@Volatile
private var instance: ModerationsManager? = null
fun initialize(context: Context) {
if (instance == null) {
synchronized(this) {
if (instance == null) {
instance = ModerationsManager(context.applicationContext)
}
}
}
}
fun getInstance(): ModerationsManager {
return instance ?: throw IllegalStateException("ModerationsManager not initialized")
}
}
}
@@ -707,14 +707,21 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
return;
Logger.i(TAG, "handleIntent started by " + intent.action);
var targetData: String? = null;
when (intent.action) {
Intent.ACTION_SEND -> {
targetData = intent.getStringExtra(Intent.EXTRA_STREAM)
?: intent.getStringExtra(Intent.EXTRA_TEXT);
Logger.i(TAG, "Share Received: " + targetData);
val streamExtra = intent.getStringExtra(Intent.EXTRA_STREAM);
val textExtra = intent.getStringExtra(Intent.EXTRA_TEXT);
val streamParcelable = intent.getParcelableExtra<android.net.Uri>(Intent.EXTRA_STREAM);
// Try to get the actual file content
targetData = when {
streamParcelable != null -> streamParcelable.toString()
streamExtra != null -> streamExtra
textExtra != null -> textExtra
else -> null
}
}
Intent.ACTION_VIEW -> {
@@ -1002,6 +1009,14 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
fun handleUnknownText(text: String): Boolean {
try {
// Check for Polycentric profile data
if (text.startsWith("polycentric://")) {
startActivity(Intent(this, PolycentricImportProfileActivity::class.java).apply {
putExtra("url", text.trim())
})
return true;
}
if (text.startsWith("@/Subscription") || text.startsWith("Subscriptions")) {
val lines = text.split("\n").map { it.trim() }.drop(1).filter { it.isNotEmpty() };
navigate(_fragImportSubscriptions, lines);
@@ -13,15 +13,18 @@ import android.view.View
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.TextView
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateApp.Companion.withContext
import com.futo.platformplayer.states.StatePolycentric
import com.futo.platformplayer.views.buttons.BigButton
import com.futo.platformplayer.activities.QRCodeFullscreenActivity
import com.futo.polycentric.core.ContentType
import com.futo.polycentric.core.SignedEvent
import com.futo.polycentric.core.StorageTypeCRDTItem
@@ -29,8 +32,10 @@ import com.futo.polycentric.core.StorageTypeCRDTSetItem
import com.futo.polycentric.core.Store
import com.futo.polycentric.core.toBase64Url
import com.google.zxing.BarcodeFormat
import com.google.zxing.EncodeHintType
import com.google.zxing.MultiFormatWriter
import com.google.zxing.common.BitMatrix
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -41,11 +46,27 @@ import userpackage.Protocol.URLInfo
class PolycentricBackupActivity : AppCompatActivity() {
private lateinit var _buttonShare: BigButton;
private lateinit var _buttonCopy: BigButton;
private lateinit var _buttonExportFile: BigButton;
private lateinit var _imageQR: ImageView;
private lateinit var _exportBundle: String;
private lateinit var _textQR: TextView;
private lateinit var _textQRHint: TextView;
private lateinit var _loader: View
private val _createDocumentLauncher = registerForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) { uri ->
uri?.let { fileUri ->
try {
contentResolver.openOutputStream(fileUri)?.use { outputStream ->
outputStream.write(_exportBundle.toByteArray())
}
UIDialogs.toast(this, getString(R.string.profile_saved_successfully))
} catch (e: Exception) {
Logger.e(TAG, "Failed to write to document", e)
UIDialogs.toast(this, "Failed to save profile: ${e.message}")
}
}
}
override fun attachBaseContext(newBase: Context?) {
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
}
@@ -57,8 +78,10 @@ class PolycentricBackupActivity : AppCompatActivity() {
_buttonShare = findViewById(R.id.button_share)
_buttonCopy = findViewById(R.id.button_copy)
_buttonExportFile = findViewById(R.id.button_export_file)
_imageQR = findViewById(R.id.image_qr)
_textQR = findViewById(R.id.text_qr)
_textQRHint = findViewById(R.id.text_qr_hint)
_loader = findViewById(R.id.progress_loader)
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
finish();
@@ -66,14 +89,23 @@ class PolycentricBackupActivity : AppCompatActivity() {
_imageQR.visibility = View.INVISIBLE
_textQR.visibility = View.INVISIBLE
_textQRHint.visibility = View.INVISIBLE
_loader.visibility = View.VISIBLE
_buttonShare.visibility = View.INVISIBLE
_buttonCopy.visibility = View.INVISIBLE
_buttonExportFile.visibility = View.INVISIBLE
lifecycleScope.launch {
val bundle = withContext(Dispatchers.IO) { createExportBundle() }
_exportBundle = bundle
Logger.i(TAG, "Export bundle created, length: ${bundle.length}")
try {
val pair = withContext(Dispatchers.IO) {
val bundle = createExportBundle()
if (!isContentSuitableForQRCode(bundle)) {
throw Exception("Data too big for QR code generation")
}
val dimension = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, 200f, resources.displayMetrics
).toInt()
@@ -81,18 +113,35 @@ class PolycentricBackupActivity : AppCompatActivity() {
Pair(bundle, qr)
}
_exportBundle = pair.first
_imageQR.setImageBitmap(pair.second)
_imageQR.visibility = View.VISIBLE
_textQR.visibility = View.VISIBLE
_textQRHint.visibility = View.VISIBLE
_buttonShare.visibility = View.VISIBLE
_buttonCopy.visibility = View.VISIBLE
_imageQR.setOnClickListener {
val intent = QRCodeFullscreenActivity.createIntent(this@PolycentricBackupActivity, _exportBundle)
startActivity(intent)
}
} catch (e: Exception) {
Logger.e(TAG, getString(R.string.failed_to_generate_qr_code), e)
val byteSize = bundle.toByteArray(Charsets.UTF_8).size
Logger.e(TAG, "QR code generation failed. Bundle length: ${bundle.length} chars, ${byteSize} bytes, Error: ${e.message}", e)
if (e.message?.contains("Data too big") == true) {
_textQR.text = getString(R.string.qr_code_too_large_use_file_export)
_buttonExportFile.visibility = View.VISIBLE
} else {
_textQR.text = getString(R.string.failed_to_generate_qr_code)
}
_textQR.visibility = View.VISIBLE
_textQRHint.visibility = View.INVISIBLE
_buttonShare.visibility = View.VISIBLE
_buttonCopy.visibility = View.VISIBLE
// Hide QR image since generation failed
_imageQR.visibility = View.INVISIBLE
_textQR.visibility = View.INVISIBLE
_buttonShare.visibility = View.INVISIBLE
_buttonCopy.visibility = View.INVISIBLE
} finally {
_loader.visibility = View.GONE
}
@@ -108,11 +157,29 @@ class PolycentricBackupActivity : AppCompatActivity() {
val clip = ClipData.newPlainText(getString(R.string.copied_text), _exportBundle);
clipboard.setPrimaryClip(clip);
};
_buttonExportFile.onClick.subscribe {
val fileName = "polycentric_profile_${System.currentTimeMillis()}.txt"
_createDocumentLauncher.launch(fileName)
};
}
private fun isContentSuitableForQRCode(content: String): Boolean {
val bytes = content.toByteArray(Charsets.UTF_8)
return bytes.size <= 2300 // QR Code Version 40 with Error Correction Level M can hold ~2331 bytes
}
private fun generateQRCode(content: String, width: Int, height: Int): Bitmap {
val bitMatrix = MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, width, height);
return bitMatrixToBitmap(bitMatrix);
if (!isContentSuitableForQRCode(content)) {
throw Exception("Data too big for QR code generation")
}
val hints = java.util.EnumMap<EncodeHintType, Any>(EncodeHintType::class.java)
hints[EncodeHintType.ERROR_CORRECTION] = ErrorCorrectionLevel.M
hints[EncodeHintType.MARGIN] = 1
val bitMatrix = MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, width, height, hints)
return bitMatrixToBitmap(bitMatrix)
}
private fun bitMatrixToBitmap(matrix: BitMatrix): Bitmap {
@@ -203,9 +270,11 @@ class PolycentricBackupActivity : AppCompatActivity() {
.setBody(exportBundle.toByteString())
.build();
return "polycentric://" + urlInfo.toByteArray().toBase64Url()
val data = urlInfo.toByteArray()
return "polycentric://" + data.toBase64Url()
}
companion object {
private const val TAG = "PolycentricBackupActivity";
}
@@ -34,6 +34,7 @@ import userpackage.Protocol.ExportBundle
class PolycentricImportProfileActivity : AppCompatActivity() {
private lateinit var _buttonHelp: ImageButton;
private lateinit var _buttonScanProfile: LinearLayout;
private lateinit var _buttonImportFile: LinearLayout;
private lateinit var _buttonImportProfile: LinearLayout;
private lateinit var _editProfile: EditText;
private lateinit var _loaderOverlay: LoaderOverlay;
@@ -48,6 +49,56 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
}
}
private val _filePickerLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { uri ->
uri?.let { fileUri ->
try {
// Check file size before reading
val fileSize = contentResolver.openFileDescriptor(fileUri, "r")?.statSize ?: 0
val maxFileSize = 10 * 1024 * 1024 // 10MB limit
if (fileSize > maxFileSize) {
UIDialogs.toast(this, "File too large. Maximum size is 10MB.")
return@let
}
if (fileSize == 0L) {
UIDialogs.toast(this, "Selected file is empty.")
return@let
}
val content = contentResolver.openInputStream(fileUri)?.bufferedReader()?.readText()
content?.let { fileContent ->
val trimmedContent = fileContent.trim()
// Check if content is empty after trimming
if (trimmedContent.isEmpty()) {
UIDialogs.toast(this, "Selected file contains no data.")
return@let
}
// Check if content looks like a valid polycentric URL
if (!trimmedContent.startsWith("polycentric://")) {
UIDialogs.toast(this, "Selected file does not contain a valid polycentric profile URL.")
return@let
}
import(trimmedContent)
} ?: run {
UIDialogs.toast(this, "Could not read file content.")
}
} catch (e: SecurityException) {
Logger.e(TAG, "Security exception reading file", e)
UIDialogs.toast(this, "Permission denied to read file.")
} catch (e: OutOfMemoryError) {
Logger.e(TAG, "Out of memory reading file", e)
UIDialogs.toast(this, "File too large to process.")
} catch (e: Exception) {
Logger.e(TAG, "Failed to read file", e)
UIDialogs.toast(this, "Failed to read file: ${e.message}")
}
}
}
override fun attachBaseContext(newBase: Context?) {
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
}
@@ -59,6 +110,7 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
_buttonHelp = findViewById(R.id.button_help);
_buttonScanProfile = findViewById(R.id.button_scan_profile);
_buttonImportFile = findViewById(R.id.button_import_file);
_buttonImportProfile = findViewById(R.id.button_import_profile);
_loaderOverlay = findViewById(R.id.loader_overlay);
_editProfile = findViewById(R.id.edit_profile);
@@ -82,6 +134,10 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
_qrCodeResultLauncher.launch(integrator.createScanIntent())
};
_buttonImportFile.setOnClickListener {
_filePickerLauncher.launch("text/plain")
};
_buttonImportProfile.setOnClickListener {
if (_editProfile.text.isEmpty()) {
UIDialogs.toast(this, getString(R.string.text_field_does_not_contain_any_data));
@@ -109,6 +165,7 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
try {
val data = url.substring("polycentric://".length).base64UrlToByteArray();
val urlInfo = Protocol.URLInfo.parseFrom(data);
if (urlInfo.urlType != 3L) {
throw Exception("Expected urlInfo struct of type ExportBundle")
}
@@ -0,0 +1,156 @@
package com.futo.platformplayer.activities
import android.content.Context
import android.os.Bundle
import android.widget.ImageButton
import android.widget.SeekBar
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import com.futo.platformplayer.ModerationsManager
import com.futo.platformplayer.R
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.setNavigationBarColorAndIcons
class PolycentricModerationActivity : AppCompatActivity() {
private lateinit var _seekbarOffensive: SeekBar
private lateinit var _seekbarExplicit: SeekBar
private lateinit var _seekbarViolence: SeekBar
private lateinit var _textOffensiveDesc: TextView
private lateinit var _textExplicitDesc: TextView
private lateinit var _textViolenceDesc: TextView
private lateinit var _textOffensiveValue: TextView
private lateinit var _textExplicitValue: TextView
private lateinit var _textViolenceValue: TextView
private lateinit var _moderationsManager: ModerationsManager
override fun attachBaseContext(newBase: Context?) {
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_polycentric_moderation)
setNavigationBarColorAndIcons()
try {
_moderationsManager = ModerationsManager.getInstance()
} catch (e: IllegalStateException) {
// ModerationsManager not initialized, finish activity
finish()
return
}
_seekbarOffensive = findViewById(R.id.seekbar_offensive)
_seekbarExplicit = findViewById(R.id.seekbar_explicit)
_seekbarViolence = findViewById(R.id.seekbar_violence)
_textOffensiveDesc = findViewById(R.id.text_offensive_desc)
_textExplicitDesc = findViewById(R.id.text_explicit_desc)
_textViolenceDesc = findViewById(R.id.text_violence_desc)
_textOffensiveValue = findViewById(R.id.text_offensive_value)
_textExplicitValue = findViewById(R.id.text_explicit_value)
_textViolenceValue = findViewById(R.id.text_violence_value)
findViewById<ImageButton>(R.id.button_back)?.setOnClickListener {
finish()
}
loadSettings()
setupListeners()
}
private fun loadSettings() {
if (isFinishing || isDestroyed) return
val levels = _moderationsManager.moderationLevels.value ?: mapOf()
val offensiveLevel = levels["hate"] ?: 2
val explicitLevel = levels["sexual"] ?: 1
val violenceLevel = levels["violence"] ?: 1
_seekbarOffensive.progress = offensiveLevel
_seekbarExplicit.progress = explicitLevel
_seekbarViolence.progress = violenceLevel
updateDescriptionText(_seekbarOffensive, _textOffensiveDesc, _textOffensiveValue, getOffensiveDescriptions())
updateDescriptionText(_seekbarExplicit, _textExplicitDesc, _textExplicitValue, getExplicitDescriptions())
updateDescriptionText(_seekbarViolence, _textViolenceDesc, _textViolenceValue, getViolenceDescriptions())
}
private fun setupListeners() {
_seekbarOffensive.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
if (isFinishing || isDestroyed) return
updateDescriptionText(seekBar, _textOffensiveDesc, _textOffensiveValue, getOffensiveDescriptions())
if (fromUser) {
_moderationsManager.setModerationLevel("hate", progress)
}
}
override fun onStartTrackingTouch(seekBar: SeekBar?) {}
override fun onStopTrackingTouch(seekBar: SeekBar?) {}
})
_seekbarExplicit.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
if (isFinishing || isDestroyed) return
updateDescriptionText(seekBar, _textExplicitDesc, _textExplicitValue, getExplicitDescriptions())
if (fromUser) {
_moderationsManager.setModerationLevel("sexual", progress)
}
}
override fun onStartTrackingTouch(seekBar: SeekBar?) {}
override fun onStopTrackingTouch(seekBar: SeekBar?) {}
})
_seekbarViolence.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
if (isFinishing || isDestroyed) return
updateDescriptionText(seekBar, _textViolenceDesc, _textViolenceValue, getViolenceDescriptions())
if (fromUser) {
_moderationsManager.setModerationLevel("violence", progress)
}
}
override fun onStartTrackingTouch(seekBar: SeekBar?) {}
override fun onStopTrackingTouch(seekBar: SeekBar?) {}
})
}
private fun updateDescriptionText(seekBar: SeekBar?, textDesc: TextView, textValue: TextView, descriptions: Array<String>) {
if (isFinishing || isDestroyed) return
val progress = seekBar?.progress ?: 0
if (progress in descriptions.indices) {
textDesc.text = descriptions[progress]
textValue.text = progress.toString()
}
}
private fun getOffensiveDescriptions(): Array<String> {
return arrayOf(
"Neutral, general terms, no bias or hate.",
"Mildly sensitive, factual.",
"Potentially offensive content",
"Offensive content"
)
}
private fun getExplicitDescriptions(): Array<String> {
return arrayOf(
"No explicit content",
"Mildly suggestive, factual or educational",
"Moderate sexual content, non-graphic",
"Explicit sexual content"
)
}
private fun getViolenceDescriptions(): Array<String> {
return arrayOf(
"Non-violent",
"Mild violence, factual or contextual",
"Moderate violence, some graphic content.",
"Graphic violence"
)
}
}
@@ -49,7 +49,7 @@ class PolycentricProfileActivity : AppCompatActivity() {
private lateinit var _buttonHelp: ImageButton;
private lateinit var _editName: EditText;
private lateinit var _buttonExport: BigButton;
private lateinit var _buttonOpenHarborProfile: BigButton;
private lateinit var _buttonModeration: BigButton;
private lateinit var _buttonLogout: BigButton;
private lateinit var _buttonDelete: BigButton;
private lateinit var _username: String;
@@ -71,7 +71,7 @@ class PolycentricProfileActivity : AppCompatActivity() {
_imagePolycentric = findViewById(R.id.image_polycentric);
_editName = findViewById(R.id.edit_profile_name);
_buttonExport = findViewById(R.id.button_export);
_buttonOpenHarborProfile = findViewById(R.id.button_open_harbor_profile);
_buttonModeration = findViewById(R.id.button_moderation);
_buttonLogout = findViewById(R.id.button_logout);
_buttonDelete = findViewById(R.id.button_delete);
_loaderOverlay = findViewById(R.id.loader_overlay);
@@ -99,15 +99,9 @@ class PolycentricProfileActivity : AppCompatActivity() {
startActivity(Intent(this, PolycentricBackupActivity::class.java));
};
_buttonOpenHarborProfile.onClick.subscribe {
val processHandle = StatePolycentric.instance.processHandle!!;
processHandle?.let {
val systemState = SystemState.fromStorageTypeSystemState(Store.instance.getSystemState(it.system));
val url = it.system.systemToURLInfoSystemLinkUrl(systemState.servers.asIterable());
val navUrl = "https://harbor.social/" + url.substring("polycentric://".length)
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(navUrl)))
}
}
_buttonModeration.onClick.subscribe {
startActivity(Intent(this, PolycentricModerationActivity::class.java));
};
_buttonLogout.onClick.subscribe {
StatePolycentric.instance.setProcessHandle(null);
@@ -0,0 +1,108 @@
package com.futo.platformplayer.activities
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Color
import android.os.Bundle
import android.util.TypedValue
import android.view.View
import android.widget.ImageButton
import android.widget.ImageView
import androidx.appcompat.app.AppCompatActivity
import com.futo.platformplayer.R
import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp
import com.google.zxing.BarcodeFormat
import com.google.zxing.EncodeHintType
import com.google.zxing.MultiFormatWriter
import com.google.zxing.common.BitMatrix
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
class QRCodeFullscreenActivity : AppCompatActivity() {
companion object {
private const val EXTRA_QR_TEXT = "qr_text"
fun createIntent(context: Context, qrText: String): android.content.Intent {
return android.content.Intent(context, QRCodeFullscreenActivity::class.java).apply {
putExtra(EXTRA_QR_TEXT, qrText)
}
}
}
override fun attachBaseContext(newBase: Context?) {
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_qr_code_fullscreen)
setNavigationBarColorAndIcons()
val qrText = intent.getStringExtra(EXTRA_QR_TEXT)
val imageQR = findViewById<ImageView>(R.id.image_qr_fullscreen)
val buttonBack = findViewById<ImageButton>(R.id.button_back_fullscreen)
val buttonClose = findViewById<ImageButton>(R.id.button_close_fullscreen)
// Generate QR code bitmap from text
qrText?.let { text ->
try {
if (!isContentSuitableForQRCode(text)) {
throw Exception("Data too big for QR code generation")
}
val dimension = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, 300f, resources.displayMetrics
).toInt()
val qrBitmap = generateQRCode(text, dimension, dimension)
imageQR.setImageBitmap(qrBitmap)
} catch (e: Exception) {
// If QR generation fails, show error or fallback
imageQR.setImageResource(R.drawable.ic_qr)
}
}
buttonBack.setOnClickListener {
finish()
}
buttonClose.setOnClickListener {
finish()
}
imageQR.setOnClickListener {
finish()
}
}
private fun isContentSuitableForQRCode(content: String): Boolean {
val bytes = content.toByteArray(Charsets.UTF_8)
return bytes.size <= 2300 // QR Code Version 40 with Error Correction Level M can hold ~2331 bytes
}
private fun generateQRCode(content: String, width: Int, height: Int): Bitmap {
if (!isContentSuitableForQRCode(content)) {
throw Exception("Data too big for QR code generation")
}
val hints = java.util.EnumMap<EncodeHintType, Any>(EncodeHintType::class.java)
hints[EncodeHintType.ERROR_CORRECTION] = ErrorCorrectionLevel.M
hints[EncodeHintType.MARGIN] = 1
val bitMatrix = MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, width, height, hints)
return bitMatrixToBitmap(bitMatrix)
}
private fun bitMatrixToBitmap(matrix: BitMatrix): Bitmap {
val width = matrix.width
val height = matrix.height
val bmp = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565)
for (x in 0 until width) {
for (y in 0 until height) {
bmp.setPixel(x, y, if (matrix[x, y]) Color.BLACK else Color.WHITE)
}
}
return bmp
}
}
@@ -21,6 +21,7 @@ import com.futo.platformplayer.*
import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.views.LoaderView
import com.futo.platformplayer.views.fields.FieldForm
import com.futo.platformplayer.views.fields.ReadOnlyTextField
@@ -114,6 +115,11 @@ class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
var isFirstLoad = true;
fun reloadSettings() {
// Ensure files are initialized before accessing Settings
if (!FragmentedStorage.isInitialized) {
FragmentedStorage.initialize(filesDir);
}
val firstLoad = isFirstLoad;
isFirstLoad = false;
_form.setSearchVisible(false);
@@ -149,6 +155,11 @@ class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
}
fun updateDevMode() {
// Ensure files are initialized before accessing SettingsDev
if (!FragmentedStorage.isInitialized) {
FragmentedStorage.initialize(filesDir);
}
if(SettingsDev.instance.developerMode)
_devSets.visibility = View.VISIBLE;
else
@@ -21,6 +21,7 @@ import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.activities.PolycentricHomeActivity
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment
import com.futo.platformplayer.api.media.models.comments.LazyComment
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
import com.futo.platformplayer.constructs.TaskHandler
import com.futo.platformplayer.logging.Logger
@@ -170,8 +171,8 @@ class CommentsFragment : MainFragment() {
return@showConfirmationDialog
}
val index = _comments.indexOf(comment)
if (index != -1) {
val index = _comments.indexOfFirst { it == comment || (it is LazyComment && it.getUnderlyingComment() == comment) }
if (index >= 0) {
_comments.removeAt(index)
_adapterComments.notifyItemRemoved(_adapterComments.childToParentPosition(index))
@@ -49,6 +49,7 @@ import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.v2.ManagedStore
import com.futo.platformplayer.views.ToastView
import com.futo.polycentric.core.ApiMethods
import com.futo.polycentric.core.toBase64Url
import kotlinx.coroutines.*
import java.io.File
import java.util.*
@@ -385,6 +386,26 @@ class StateApp {
_cacheDirectory?.let { ApiMethods.initCache(it) };
}
Logger.i(TAG, "MainApp Starting: Initializing [ModerationsManager]");
ModerationsManager.initialize(context);
Logger.i(TAG, "MainApp Starting: Setting [ModerationLevelProvider]");
ApiMethods.setModerationLevelProvider {
try {
ModerationsManager.getInstance().getCurrentModerationLevels()
} catch (e: IllegalStateException) {
null
}
}
ApiMethods.setModerationExemptSystemProvider {
try {
StatePolycentric.instance.processHandle?.system?.toProto()?.toByteArray()?.toBase64Url()
} catch (e: Throwable) {
null
}
}
val logFile = File(context.filesDir, "log.txt");
if (Settings.instance.logging.logLevel > LogLevel.NONE.value) {
val fileLogConsumer = FileLogConsumer(logFile, LogLevel.fromInt(Settings.instance.logging.logLevel), false);
@@ -28,6 +28,7 @@ import com.futo.platformplayer.selectBestImage
import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.StringStorage
import com.futo.polycentric.core.ApiMethods
import com.futo.polycentric.core.ensureServerAndBackfill
import com.futo.polycentric.core.ClaimType
import com.futo.polycentric.core.ContentType
import com.futo.polycentric.core.Opinion
@@ -46,8 +47,10 @@ import com.google.protobuf.ByteString
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import userpackage.Protocol
import userpackage.Protocol.Reference
@@ -67,6 +70,8 @@ class StatePolycentric {
private val _commentPool = ForkJoinPool(2);
private val _commentPoolDispatcher = _commentPool.asCoroutineDispatcher();
private val _backgroundJob = SupervisorJob()
private val _backgroundScope = CoroutineScope(_backgroundJob + Dispatchers.IO)
fun load(context: Context) {
if (!enabled) {
@@ -173,6 +178,15 @@ class StatePolycentric {
}
_likeDislikeMap = newMap
// Ensure current server is registered & synced (runs in background)
_backgroundScope.launch {
try {
processHandle.ensureServerAndBackfill()
} catch (e: Throwable) {
Logger.w(TAG, "Failed to ensure server and backfill: "+e.message)
}
}
} else {
_activeProcessHandle.setAndSave("");
_likeDislikeMap = hashMapOf()
@@ -559,6 +573,11 @@ class StatePolycentric {
};
}
fun cleanup() {
_backgroundJob.cancel()
_commentPool.shutdown()
}
companion object {
private const val TAG = "StatePolycentric";
@@ -161,8 +161,8 @@ class CommentsList : ConstraintLayout {
return@showConfirmationDialog
}
val index = _comments.indexOf(comment)
if (index != -1) {
val index = _comments.indexOfFirst { it == comment || (it is LazyComment && it.getUnderlyingComment() == comment) }
if (index >= 0) {
_comments.removeAt(index)
_adapterComments.notifyItemRemoved(_adapterComments.childToParentPosition(index))
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/colorPrimary" />
<corners android:radius="18dp" />
</shape>
@@ -29,6 +29,8 @@
android:id="@+id/image_qr"
android:layout_width="200dp"
android:layout_height="200dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:padding="8dp"
app:srcCompat="@drawable/ic_qr"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
@@ -37,15 +39,31 @@
<TextView
android:id="@+id/text_qr"
android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/scan_to_import"
android:fontFamily="@font/inter_light"
android:textSize="32dp"
android:textAlignment="center"
android:layout_marginTop="12dp"
android:layout_marginStart="20dp"
android:layout_marginEnd="20dp"
app:layout_constraintTop_toBottomOf="@id/image_qr"
app:layout_constraintLeft_toLeftOf="@id/image_qr"
app:layout_constraintRight_toRightOf="@id/image_qr" />
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" />
<TextView
android:id="@+id/text_qr_hint"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/tap_qr_code_for_fullscreen"
android:fontFamily="@font/inter_light"
android:textSize="14dp"
android:textColor="@color/gray_400"
android:layout_marginTop="8dp"
app:layout_constraintTop_toBottomOf="@id/text_qr"
app:layout_constraintLeft_toLeftOf="@id/text_qr"
app:layout_constraintRight_toRightOf="@id/text_qr" />
<LinearLayout
android:id="@+id/layout_buttons"
@@ -55,7 +73,7 @@
android:layout_marginStart="20dp"
android:layout_marginEnd="20dp"
android:layout_marginTop="30dp"
app:layout_constraintTop_toBottomOf="@id/text_qr"
app:layout_constraintTop_toBottomOf="@id/text_qr_hint"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent">
@@ -75,6 +93,15 @@
app:buttonSubText="@string/copy_your_identity_to_clipboard"
app:buttonIcon="@drawable/ic_copy"
android:layout_marginTop="8dp" />
<com.futo.platformplayer.views.buttons.BigButton
android:id="@+id/button_export_file"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:buttonText="@string/export_to_file"
app:buttonSubText="@string/save_profile_to_file_for_sharing"
app:buttonIcon="@drawable/ic_download"
android:layout_marginTop="8dp" />
</LinearLayout>
<ProgressBar
@@ -47,6 +47,28 @@
android:text="@string/scan_qr" />
</LinearLayout>
<LinearLayout
android:id="@+id/button_import_file"
android:layout_width="140dp"
android:layout_height="40dp"
android:background="@drawable/background_button_primary_round"
android:gravity="center"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:orientation="horizontal"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/button_scan_profile">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/inter_light"
android:textColor="@color/white"
android:textSize="16dp"
android:text="@string/import_from_file" />
</LinearLayout>
<TextView
android:id="@+id/text_or"
android:layout_width="wrap_content"
@@ -55,7 +77,7 @@
android:fontFamily="@font/inter_light"
android:textSize="28dp"
android:layout_marginTop="30dp"
app:layout_constraintTop_toBottomOf="@id/button_scan_profile"
app:layout_constraintTop_toBottomOf="@id/button_import_file"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" />
@@ -0,0 +1,230 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black"
android:padding="16dp">
<LinearLayout
android:id="@+id/header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintTop_toTopOf="parent">
<ImageButton
android:id="@+id/button_back"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/back"
android:padding="12dp"
android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_back_thin_white_16dp" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="1"
android:text="Moderation Settings"
android:textColor="?attr/colorOnBackground"
android:textSize="18sp"
android:textStyle="bold" />
</LinearLayout>
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/header">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<!-- BEGIN BLURB -->
<TextView
android:id="@+id/text_moderation_blurb"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:text="A lower slider value hides more content. Posts or comments with a tag level ABOVE your selected value will be hidden. (Level 0 = most strict, Level 3 = allow everything)"
android:textColor="?attr/colorOnSurface"
android:textSize="14sp" />
<!-- END BLURB -->
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardBackgroundColor="?attr/colorSurface"
app:cardCornerRadius="16dp"
app:cardElevation="2dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Moderation Levels"
android:textColor="?attr/colorOnSurface"
android:textSize="18sp"
android:textStyle="bold" />
<!-- Offensive Content -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Offensive Content"
android:textColor="?attr/colorOnSurface"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/text_offensive_desc"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="Description"
android:textColor="?attr/colorOnSurface"
android:textSize="14sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<SeekBar
android:id="@+id/seekbar_offensive"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:max="3"
android:progress="2" />
<TextView
android:id="@+id/text_offensive_value"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:background="@drawable/bg_slider_value"
android:gravity="center"
android:minWidth="36dp"
android:padding="8dp"
android:text="2"
android:textColor="@android:color/white"
android:textStyle="bold" />
</LinearLayout>
<!-- Explicit Content -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="Explicit Content"
android:textColor="?attr/colorOnSurface"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/text_explicit_desc"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="Description"
android:textColor="?attr/colorOnSurface"
android:textSize="14sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<SeekBar
android:id="@+id/seekbar_explicit"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:max="3"
android:progress="1" />
<TextView
android:id="@+id/text_explicit_value"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:background="@drawable/bg_slider_value"
android:gravity="center"
android:minWidth="36dp"
android:padding="8dp"
android:text="1"
android:textColor="@android:color/white"
android:textStyle="bold" />
</LinearLayout>
<!-- Violence -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="Violence"
android:textColor="?attr/colorOnSurface"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/text_violence_desc"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="Description"
android:textColor="?attr/colorOnSurface"
android:textSize="14sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<SeekBar
android:id="@+id/seekbar_violence"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:max="3"
android:progress="1" />
<TextView
android:id="@+id/text_violence_value"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:background="@drawable/bg_slider_value"
android:gravity="center"
android:minWidth="36dp"
android:padding="8dp"
android:text="1"
android:textColor="@android:color/white"
android:textStyle="bold" />
</LinearLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>
</LinearLayout>
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -73,7 +73,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/inter_regular"
android:text="Further customize your profile, make platform claims, and other creator-specific features in the Harbor app."
android:text="Further customize your profile, make platform claims, and other creator-specific features in the Polycentric app."
android:textSize="12dp"
android:linksClickable="true"
android:paddingLeft="20dp"
@@ -90,7 +90,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/inter_regular"
android:text="https://harbor.social"
android:text="https://polycentric.io"
android:textSize="12dp"
android:linksClickable="true"
android:paddingLeft="20dp"
@@ -107,7 +107,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/inter_regular"
android:text="After you've installed Harbor you can export this profile to Harbor using the Export button."
android:text="After you've installed Polycentric you can export this profile to Polycentric using the Export button."
android:textSize="12dp"
android:linksClickable="true"
android:paddingLeft="20dp"
@@ -133,13 +133,13 @@
app:layout_constraintBottom_toBottomOf="parent">
<com.futo.platformplayer.views.buttons.BigButton
android:id="@+id/button_open_harbor_profile"
android:id="@+id/button_moderation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:buttonText="Harbor Profile"
app:buttonSubText="See your Harbor profile in a browser"
app:buttonIcon="@drawable/ic_export"
android:layout_marginTop="8dp" />
app:buttonSubText="Set moderation settings for polycentric comments"
android:layout_marginTop="8dp"
app:buttonIcon="@drawable/ic_settings"
app:buttonText="Moderation Settings" />
<com.futo.platformplayer.views.buttons.BigButton
android:id="@+id/button_export"
@@ -168,6 +168,7 @@
app:buttonIcon="@drawable/ic_trash"
android:layout_marginTop="8dp"
app:buttonBackground="@drawable/background_big_button_red"/>
</LinearLayout>
<com.futo.platformplayer.views.overlays.LoaderOverlay
@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black">
<!-- Top navigation bar -->
<ImageButton
android:id="@+id/button_back_fullscreen"
android:layout_width="50dp"
android:layout_height="50dp"
android:contentDescription="@string/cd_button_back"
android:padding="10dp"
android:scaleType="fitCenter"
android:background="?android:attr/selectableItemBackgroundBorderless"
app:srcCompat="@drawable/ic_back_thin_white_16dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
android:layout_marginTop="16dp"
android:layout_marginStart="16dp" />
<ImageButton
android:id="@+id/button_close_fullscreen"
android:layout_width="50dp"
android:layout_height="50dp"
android:contentDescription="@string/cd_button_close"
android:padding="10dp"
android:scaleType="fitCenter"
android:background="?android:attr/selectableItemBackgroundBorderless"
app:srcCompat="@drawable/ic_close"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp" />
<!-- Full screen QR code -->
<ImageView
android:id="@+id/image_qr_fullscreen"
android:layout_width="0dp"
android:layout_height="0dp"
android:scaleType="fitCenter"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:padding="40dp"
app:layout_constraintTop_toBottomOf="@id/button_back_fullscreen"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:layout_marginTop="20dp"
android:layout_marginBottom="20dp"
android:layout_marginStart="20dp"
android:layout_marginEnd="20dp" />
</androidx.constraintlayout.widget.ConstraintLayout>
+2
View File
@@ -404,6 +404,8 @@
<string name="unknown_reconstruction_type">Unbekannter Rekonstruktionstyp</string>
<string name="failed_to_parse_newpipe_subscriptions">Fehler beim Parsen von NewPipe-Abonnements</string>
<string name="failed_to_generate_qr_code">Fehler beim Generieren des QR-Codes</string>
<string name="qr_code_too_large_use_text_below">QR-Code zu groß. Verwenden Sie den Text unten, um Ihr Profil zu teilen.</string>
<string name="tap_qr_code_for_fullscreen">Tippen Sie auf den QR-Code für Vollbildansicht</string>
<string name="share_text">Text teilen</string>
<string name="copied_text">Text kopiert</string>
<string name="must_be_at_least_3_characters_long">Muss mindestens 3 Zeichen lang sein.</string>
+2
View File
@@ -381,6 +381,8 @@
<string name="unknown_reconstruction_type">Tipo de reconstrucción desconocido</string>
<string name="failed_to_parse_newpipe_subscriptions">Error al analizar las suscripciones de NewPipe</string>
<string name="failed_to_generate_qr_code">Error al generar el código QR</string>
<string name="qr_code_too_large_use_text_below">Código QR demasiado grande. Use el texto de abajo para compartir su perfil.</string>
<string name="tap_qr_code_for_fullscreen">Toca el código QR para vista completa</string>
<string name="share_text">Compartir texto</string>
<string name="copied_text">Texto copiado</string>
<string name="must_be_at_least_3_characters_long">Debe tener al menos 3 caracteres de longitud.</string>
+2
View File
@@ -420,6 +420,8 @@
<string name="unknown_reconstruction_type">Type de reconstruction inconnu</string>
<string name="failed_to_parse_newpipe_subscriptions">Échec de l\'analyse des abonnements NewPipe</string>
<string name="failed_to_generate_qr_code">Échec de la génération du code QR</string>
<string name="qr_code_too_large_use_text_below">Code QR trop volumineux. Utilisez le texte ci-dessous pour partager votre profil.</string>
<string name="tap_qr_code_for_fullscreen">Appuyez sur le code QR pour la vue plein écran</string>
<string name="share_text">Partager le texte</string>
<string name="copied_text">Texte copié</string>
<string name="must_be_at_least_3_characters_long">Doit comporter au moins 3 caractères.</string>
+12
View File
@@ -653,6 +653,18 @@
<string name="failed_to_parse_text_file">Failed to parse text file</string>
<string name="failed_to_parse_newpipe_subscriptions">Failed to parse NewPipe Subscriptions</string>
<string name="failed_to_generate_qr_code">Failed to generate QR code</string>
<string name="qr_code_too_large_use_file_export">QR code too large. Use the file export button below to share your profile.</string>
<string name="tap_qr_code_for_fullscreen">Tap QR code for fullscreen view</string>
<string name="export_to_file">Export to File</string>
<string name="save_profile_to_file_for_sharing">Save profile to file for sharing</string>
<string name="import_from_file">Import from File</string>
<string name="export_profile">Export Profile</string>
<string name="choose_export_option">Choose export option</string>
<string name="save_to_device">Save to Device</string>
<string name="share_profile">Share Profile</string>
<string name="profile_saved_to_downloads">Profile saved to Downloads</string>
<string name="failed_to_save_profile">Failed to save profile</string>
<string name="authority">com.futo.platformplayer.fileprovider</string>
<string name="share_text">Share Text</string>
<string name="copied_text">Copied Text</string>
<string name="must_be_at_least_3_characters_long">Must be at least 3 characters long.</string>
+1
View File
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<files-path name="shares" path="shares/"/>
<files-path name="files" path="." />
<external-path name="external_files" path="." />
</paths>
+3 -1
View File
@@ -6,7 +6,9 @@
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
org.gradle.jvmargs=-Xmx4096m -Dfile.encoding=UTF-8
# Kotlin daemon memory settings
kotlin.daemon.jvmargs=-Xmx4096m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects