Skip to content

Commit

Permalink
Cont Discord, artist page, and prefs, add URL open
Browse files Browse the repository at this point in the history
Use multiple custom image Discord channels for indexing
Add preferences options for the PlayerView top bar and filters #7
Move MediaItem data supplying functionality into separate classes which save by default after edit
Support opening YouTube and YouTube Music URLs (only /watch and /channel implemented currently)
Continue artist page redesign
  • Loading branch information
toasterofbread committed May 10, 2023
1 parent 1ca6340 commit ad936b0
Show file tree
Hide file tree
Showing 40 changed files with 1,213 additions and 651 deletions.
43 changes: 31 additions & 12 deletions androidApp/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
xmlns:tools="http:https://schemas.android.com/tools"
package="com.spectre7.spmp">

<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS"
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission
android:name="android.permission.WRITE_SECURE_SETTINGS"
tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
<uses-permission
android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />

<application
Expand All @@ -30,19 +32,35 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:host="music.youtube.com" android:pathPrefix="/" android:scheme="https" />
</intent-filter>
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:host="www.youtube.com" android:pathPrefix="/" android:scheme="https" />
</intent-filter>
</activity>

<activity android:name=".ErrorReportActivity" />

<service android:name=".PlayerService"
<service
android:name=".PlayerService"
android:exported="true"
android:foregroundServiceType="mediaPlayback">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</service>

<service android:label="TODO" android:name=".PlayerAccessibilityService" android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
<service
android:label="TODO"
android:name=".PlayerAccessibilityService"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
android:exported="true">
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService" />
Expand All @@ -52,11 +70,12 @@
android:resource="@xml/accessibility_service" />
</service>

<service android:name=".PlayerDownloadService"
android:exported="false">
</service>
<service
android:name=".PlayerDownloadService"
android:exported="false"></service>

<receiver android:name="androidx.media.session.MediaButtonReceiver"
<receiver
android:name="androidx.media.session.MediaButtonReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
Expand Down
8 changes: 7 additions & 1 deletion androidApp/src/main/java/com/spectre7/spmp/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import com.spectre7.spmp.model.MediaItem
import com.spectre7.spmp.platform.PlatformContext
import android.content.Intent
import android.net.Uri

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
Expand All @@ -30,8 +32,12 @@ class MainActivity : ComponentActivity() {
val context = PlatformContext(this)
SpMp.init(context)

val open_uri: Uri? =
if (intent.action == Intent.ACTION_VIEW) intent.data
else null

setContent {
SpMp.App()
SpMp.App(open_uri?.toString())
}
}

Expand Down
2 changes: 1 addition & 1 deletion shared/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ val buildConfigDebug = tasks.register("buildConfigDebug", GenerateBuildConfig::c
buildConfig(true)
}
val buildConfigRelease = tasks.register("buildConfigRelease", GenerateBuildConfig::class.java) {
buildConfig(true)
buildConfig(false)
}

tasks.all {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,38 @@ import com.my.kizzyrpc.model.Activity
import com.my.kizzyrpc.model.Assets
import com.my.kizzyrpc.model.Metadata
import com.my.kizzyrpc.model.Timestamps
import com.spectre7.utils.indexOfFirstOrNull
import com.spectre7.utils.indexOfOrNull
import dev.kord.common.entity.DiscordChannel
import dev.kord.common.entity.DiscordMessage
import dev.kord.common.entity.Snowflake
import dev.kord.core.Kord
import dev.kord.core.behavior.channel.createTextChannel
import dev.kord.core.cache.data.toData
import dev.kord.core.entity.channel.CategorizableChannel
import dev.kord.core.entity.channel.Category
import dev.kord.core.exception.KordInitializationException
import dev.kord.rest.NamedFile
import dev.kord.rest.builder.message.EmbedBuilder
import dev.kord.rest.builder.channel.TextChannelCreateBuilder
import dev.kord.rest.json.request.ChannelModifyPatchRequest
import dev.kord.rest.route.Position
import dev.kord.rest.service.ChannelService
import dev.kord.rest.service.createTextChannel
import dev.kord.rest.service.patchCategory
import io.ktor.client.request.forms.*
import io.ktor.utils.io.jvm.javaio.*
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.toList
import java.io.ByteArrayOutputStream

actual class DiscordStatus actual constructor(
private val bot_token: String?,
private val custom_images_channel_id: Long?,
private val guild_id: Long?,
private val custom_images_channel_category_id: Long?,
private val custom_images_channel_name_prefix: String,
account_token: String?
) {
actual companion object {
Expand All @@ -36,6 +54,10 @@ actual class DiscordStatus actual constructor(
if (account_token == null || account_token.isBlank()) {
throw IllegalArgumentException("Account token is required")
}
if (guild_id == null && custom_images_channel_category_id == null) {
throw IllegalArgumentException("At least one of guild_id and custom_images_channel_category_id required")
}

rpc = KizzyRPC(account_token)
}

Expand Down Expand Up @@ -91,45 +113,87 @@ actual class DiscordStatus actual constructor(

private fun getProxyUrlAttachment(proxy_url: String): String = "mp:" + Uri.parse(proxy_url).path!!.removePrefix("/")

private suspend fun ChannelService.firstMessageOrNull(channel_id: Snowflake, predicate: (DiscordMessage) -> Boolean): DiscordMessage? {
var position: Position? = null
while (true) {
val messages = getMessages(channel_id, position)
if (messages.isEmpty()) {
return null
}
for (message in messages) {
if (predicate(message)) {
return message
}
}
position = Position.After(messages.last().id)
}
}

@Suppress("UNUSED_VALUE")
actual suspend fun getCustomImage(unique_id: String, imageProvider: () -> ImageBitmap?): String? {
check(bot_token != null)
check(custom_images_channel_id != null)

var kord: Kord
try {
kord = Kord(bot_token)
}
// Handle Discord rate limit
catch (e: KordInitializationException) {
val message = e.message ?: throw e

val start = message.indexOf("retry_after")
if (start == -1) {
throw e
}

val retry_after = message.substring(start + 14, message.indexOf("\"", start + 14)).toFloatOrNull()
?: throw NotImplementedError(message)
val start = (message.indexOfOrNull("retry_after") ?: throw e) + 14
val end = message.indexOfFirstOrNull(start) { !it.isDigit() && it != '.' } ?: throw NotImplementedError(message)

val retry_after = message.substring(start, end).toFloatOrNull() ?: throw NotImplementedError(message)
delay((retry_after * 1000L).toLong())

kord = Kord(bot_token)
}

val result = with(kord.rest.channel) {
val channel = Snowflake(custom_images_channel_id)
val channel_name = custom_images_channel_name_prefix + unique_id.first()

val messages = getMessages(channel)
for (message in messages) {
if (message.author.id != kord.selfId) {
continue
}
var channel: Snowflake?
val category: Category?

// Get existing channel from category or guild
if (custom_images_channel_category_id != null) {
category = Category(getChannel(Snowflake(custom_images_channel_category_id)).toData(), kord)
channel = category.channels.firstOrNull { it.name == channel_name }?.id
}
else {
check(guild_id != null)
category = null
channel = kord.defaultSupplier.getGuildChannels(Snowflake(guild_id)).firstOrNull { it.name == channel_name }?.id
}

if (message.content == unique_id) {
// Find matching image from existing channel
if (channel != null) {
val message = firstMessageOrNull(channel) { message ->
message.author.id == kord.selfId && message.content == unique_id
}
if (message != null) {
return@with getProxyUrlAttachment(message.attachments.first().proxyUrl)
}
}

// Get image from caller
val image = imageProvider() ?: return null

// Create new channel if needed
if (channel == null) {
val channel_builder: TextChannelCreateBuilder.() -> Unit = {
this
}

ChannelModifyPatchRequest()

channel =
if (category != null) category.createTextChannel(channel_name, channel_builder).id
else kord.rest.guild.createTextChannel(Snowflake(guild_id!!), channel_name, channel_builder).id
}

// Upload image data to channel
val message = createMessage(channel) {
content = unique_id

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import androidx.compose.ui.graphics.toArgb
import com.spectre7.utils.getStringTODO
import com.spectre7.utils.getString

private const val DEFAULT_NOTIFICATION_CHANNEL_ID = "default_channel"
private const val ERROR_NOTIFICATION_CHANNEL_ID = "download_error_channel"

fun getAppName(context: Context): String {
Expand Down Expand Up @@ -136,8 +137,19 @@ actual class PlatformContext(private val context: Context) {
actual fun loadFontFromFile(path: String): Font = Font(path, ctx.resources.assets)

actual fun canSendNotifications(): Boolean = NotificationManagerCompat.from(ctx).areNotificationsEnabled()
@SuppressLint("MissingPermission")
actual fun sendNotification(title: String, body: String) {
TODO()
if (canSendNotifications()) {
val notification = Notification.Builder(context, getDefaultNotificationChannel(ctx))
.setContentTitle(title)
.setContentText(body)
.build()

NotificationManagerCompat.from(ctx).notify(
System.currentTimeMillis().toInt(),
notification
)
}
}
@SuppressLint("MissingPermission")
actual fun sendNotification(throwable: Throwable) {
Expand Down Expand Up @@ -206,6 +218,17 @@ private fun Context.findWindow(): Window? {
return null
}

private fun getDefaultNotificationChannel(context: Context): String {
val channel = NotificationChannel(
DEFAULT_NOTIFICATION_CHANNEL_ID,
getStringTODO("Default channel"),
NotificationManager.IMPORTANCE_DEFAULT
)

NotificationManagerCompat.from(context).createNotificationChannel(channel)
return DEFAULT_NOTIFICATION_CHANNEL_ID
}

private fun getErrorNotificationChannel(context: Context): String {
val channel = NotificationChannel(
ERROR_NOTIFICATION_CHANNEL_ID,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,9 @@ actual class ProjectPreferences private constructor(private val prefs: SharedPre
}
actual operator fun contains(key: String): Boolean = prefs.contains(key)

actual fun addListener(listener: Listener) {
actual fun addListener(listener: Listener): Listener {
prefs.registerOnSharedPreferenceChangeListener(listener)
return listener
}

actual fun removeListener(listener: Listener) {
Expand Down
17 changes: 15 additions & 2 deletions shared/src/commonMain/kotlin/SpMp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,11 @@ import com.spectre7.spmp.model.Settings
import com.spectre7.spmp.platform.PlatformAlertDialog
import com.spectre7.spmp.platform.ProjectPreferences
import com.spectre7.spmp.ui.layout.PlayerView
import com.spectre7.spmp.ui.layout.PlayerViewContextImpl
import com.spectre7.spmp.ui.theme.ApplicationTheme
import com.spectre7.spmp.ui.theme.Theme
import com.spectre7.utils.*
import java.net.URI
import java.util.*
import kotlin.concurrent.thread
import kotlin.math.roundToInt
Expand Down Expand Up @@ -109,13 +111,24 @@ object SpMp {
}

@Composable
fun App() {
fun App(open_uri: String?) {
ApplicationTheme(context, getFontFamily(context)) {
Theme.Update(context, MaterialTheme.colorScheme.primary)

val player = remember { PlayerViewContextImpl() }
player.init()

LaunchedEffect(open_uri) {
if (open_uri != null) {
player.openUri(open_uri).onFailure {
context.sendNotification(it)
}
}
}

Surface(modifier = Modifier.fillMaxSize()) {
if (PlayerServiceHost.service_connected) {
PlayerView()
PlayerView(player)
}
else if (!service_started) {
service_started = true
Expand Down
Loading

0 comments on commit ad936b0

Please sign in to comment.