Initial commit

This commit is contained in:
Rui Barbosa
2026-05-18 09:37:50 +01:00
commit 7685118e0b
120 changed files with 5990 additions and 0 deletions
+45
View File
@@ -0,0 +1,45 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
/coverage/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release
+45
View File
@@ -0,0 +1,45 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "2c9eb20739dfec95e2c74bd3dfa4601b0a8a36aa"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 2c9eb20739dfec95e2c74bd3dfa4601b0a8a36aa
base_revision: 2c9eb20739dfec95e2c74bd3dfa4601b0a8a36aa
- platform: android
create_revision: 2c9eb20739dfec95e2c74bd3dfa4601b0a8a36aa
base_revision: 2c9eb20739dfec95e2c74bd3dfa4601b0a8a36aa
- platform: ios
create_revision: 2c9eb20739dfec95e2c74bd3dfa4601b0a8a36aa
base_revision: 2c9eb20739dfec95e2c74bd3dfa4601b0a8a36aa
- platform: linux
create_revision: 2c9eb20739dfec95e2c74bd3dfa4601b0a8a36aa
base_revision: 2c9eb20739dfec95e2c74bd3dfa4601b0a8a36aa
- platform: macos
create_revision: 2c9eb20739dfec95e2c74bd3dfa4601b0a8a36aa
base_revision: 2c9eb20739dfec95e2c74bd3dfa4601b0a8a36aa
- platform: web
create_revision: 2c9eb20739dfec95e2c74bd3dfa4601b0a8a36aa
base_revision: 2c9eb20739dfec95e2c74bd3dfa4601b0a8a36aa
- platform: windows
create_revision: 2c9eb20739dfec95e2c74bd3dfa4601b0a8a36aa
base_revision: 2c9eb20739dfec95e2c74bd3dfa4601b0a8a36aa
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'
+83
View File
@@ -0,0 +1,83 @@
# OpenVinyl — Flutter (Android)
A free clone of the OpenVinyl turntable visualizer for Android.
## Project Structure
```
lib/
main.dart ← App entry, permission check
models/
media_info.dart ← Data class for now-playing track
services/
media_service.dart ← Platform channel bridge (Flutter side)
screens/
player_screen.dart ← Main turntable screen
permission_screen.dart ← Shown if Notification Access not granted
widgets/
vinyl_record.dart ← Spinning record with album art + grooves
tonearm.dart ← Animated needle arm
android/app/src/main/
kotlin/pt/ruifpb/openvinyl/
MainActivity.kt ← Platform channel setup (MethodChannel + EventChannel)
MediaListenerService.kt ← NotificationListenerService reads now-playing data
res/xml/
notification_listener_service.xml
AndroidManifest.xml ← Service declaration with BIND_NOTIFICATION_LISTENER
```
## Setup
### 1. Package name
By default the package is `pt.ruifpb.openvinyl`. If you want to change it:
- Update the `package` line in both `.kt` files
- Update `applicationId` in `android/app/build.gradle`
- Update the directory path under `kotlin/`
### 2. Install dependencies
```bash
flutter pub get
```
### 3. Run
```bash
flutter run
```
On first launch the app will show a permission screen. Tap **Open Settings**,
find **OpenVinyl** in the Notification Listener list, and enable it. Return to
the app — it will detect the change automatically.
---
## Adding Home Screen Widgets (later)
The code is pre-wired for this. When you're ready:
1. **Uncomment** the `home_widget` import and `_pushToWidget()` call in
`lib/services/media_service.dart`
2. Create a Glance widget in Kotlin:
`android/app/src/main/kotlin/pt/ruifpb/openvinyl/VinylWidgetReceiver.kt`
3. Add the `<receiver>` block in `AndroidManifest.xml` (stub already in the file)
4. Add widget info XML at `android/app/src/main/res/xml/vinyl_widget_info.xml`
The `home_widget` package (already in `pubspec.yaml`) handles the Flutter ↔
widget data bridge via `HomeWidget.saveWidgetData()` and
`HomeWidget.updateWidget()`.
---
## How the Notification Listener Works
`MediaListenerService` extends Android's `NotificationListenerService`.
When any app posts a notification that carries a `MediaSession.Token` extra
(Spotify, YouTube Music, Podcast apps, etc.), the service:
1. Creates a `MediaController` from the token
2. Reads metadata (title, artist, album art bitmap) and playback state
3. Serialises the bitmap to JPEG bytes
4. Pushes a `Map` through the `EventChannel` to Flutter
Flutter's `MediaService` receives the map, deserialises it into a `MediaInfo`
object, and re-broadcasts it to any listener (currently `PlayerScreen`).
+1
View File
@@ -0,0 +1 @@
include: package:flutter_lints/flutter.yaml
+14
View File
@@ -0,0 +1,14 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
.cxx/
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks
+55
View File
@@ -0,0 +1,55 @@
import java.util.Properties
import java.io.FileInputStream
plugins {
id("com.android.application")
id("kotlin-android")
id("dev.flutter.flutter-gradle-plugin")
}
val keyPropsFile = rootProject.file("key.properties")
val keyProps = Properties().apply {
if (keyPropsFile.exists()) load(FileInputStream(keyPropsFile))
}
android {
namespace = "pt.ruifpb.openvinyl"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}
defaultConfig {
applicationId = "pt.ruifpb.openvinyl"
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
signingConfigs {
create("release") {
keyAlias = keyProps["keyAlias"] as String?
keyPassword = keyProps["keyPassword"] as String?
storeFile = if (keyProps["storeFile"] != null) file(keyProps["storeFile"] as String) else null
storePassword = keyProps["storePassword"] as String?
}
}
buildTypes {
release {
signingConfig = signingConfigs.getByName("release")
}
}
}
flutter {
source = "../.."
}
@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
+84
View File
@@ -0,0 +1,84 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- android/app/src/main/AndroidManifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- NotificationListenerService does not need extra manifest permissions;
the user grants access via system settings (Notification Access). -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<application
android:label="OpenVinyl"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<!-- Main Flutter activity -->
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize"
android:screenOrientation="portrait">
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" />
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- NotificationListenerService -->
<service
android:name=".MediaListenerService"
android:label="OpenVinyl Media Listener"
android:exported="false"
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
<intent-filter>
<action android:name="android.service.notification.NotificationListenerService" />
</intent-filter>
<meta-data
android:name="android.service.notification.default_filter_types"
android:resource="@xml/notification_listener_service" />
</service>
<!-- Static vinyl widget -->
<receiver
android:name=".VinylWidgetProvider"
android:exported="true"
android:label="OpenVinyl — Vinyl">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/vinyl_widget_info" />
</receiver>
<!-- Animated vinyl widget -->
<receiver
android:name=".VinylAnimatedWidgetProvider"
android:exported="true"
android:label="OpenVinyl — Vinyl (Animated)">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/vinyl_widget_animated_info" />
</receiver>
<!-- Foreground service that drives the animated widget frame loop -->
<service
android:name=".VinylAnimatedWidgetService"
android:exported="false"
android:foregroundServiceType="mediaPlayback" />
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
</manifest>
@@ -0,0 +1,56 @@
// android/app/src/main/kotlin/pt/ruifpb/openvinyl/MainActivity.kt
//
// MethodChannel pt.ruifpb.openvinyl/media_control
// hasPermission() → Boolean
// requestPermission() → Unit
// getCurrentMedia() → Map?
// playPause() → Unit ← NEW: sends play/pause to active MediaSession
package pt.ruifpb.openvinyl
import android.content.Intent
import android.provider.Settings
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodChannel
class MainActivity : FlutterActivity() {
private val methodChannelName = "pt.ruifpb.openvinyl/media_control"
private val eventChannelName = "pt.ruifpb.openvinyl/media_events"
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, methodChannelName)
.setMethodCallHandler { call, result ->
when (call.method) {
"hasPermission" -> result.success(MediaListenerService.isEnabled(this))
"requestPermission" -> {
startActivity(
Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
)
result.success(null)
}
"getCurrentMedia" -> result.success(MediaListenerService.lastMediaMap)
"playPause" -> {
MediaListenerService.sendPlayPause()
result.success(null)
}
else -> result.notImplemented()
}
}
EventChannel(flutterEngine.dartExecutor.binaryMessenger, eventChannelName)
.setStreamHandler(object : EventChannel.StreamHandler {
override fun onListen(args: Any?, sink: EventChannel.EventSink) {
MediaListenerService.eventSink = sink
}
override fun onCancel(args: Any?) {
MediaListenerService.eventSink = null
}
})
}
}
@@ -0,0 +1,129 @@
// android/app/src/main/kotlin/pt/ruifpb/openvinyl/MediaListenerService.kt
package pt.ruifpb.openvinyl
import android.app.Notification
import android.content.ComponentName
import android.content.Context
import android.graphics.Bitmap
import android.media.MediaMetadata
import android.media.session.MediaController
import android.media.session.MediaSession
import android.media.session.PlaybackState
import android.service.notification.NotificationListenerService
import android.service.notification.StatusBarNotification
import io.flutter.plugin.common.EventChannel
import java.io.ByteArrayOutputStream
class MediaListenerService : NotificationListenerService() {
companion object {
var eventSink: EventChannel.EventSink? = null
var lastMediaMap: Map<String, Any?>? = null
// Hold the active MediaController so MainActivity can send
// play/pause commands through it
var activeController: MediaController? = null
fun isEnabled(context: Context): Boolean {
val flat = android.provider.Settings.Secure.getString(
context.contentResolver,
"enabled_notification_listeners"
) ?: return false
val cn = ComponentName(context, MediaListenerService::class.java)
return flat.contains(cn.flattenToString())
}
// Called by MainActivity via the platform channel
fun sendPlayPause() {
val ctrl = activeController ?: return
val state = ctrl.playbackState?.state
if (state == PlaybackState.STATE_PLAYING) {
ctrl.transportControls.pause()
} else {
ctrl.transportControls.play()
}
}
}
override fun onNotificationPosted(sbn: StatusBarNotification) {
processNotification(sbn)
}
override fun onNotificationRemoved(sbn: StatusBarNotification) {
if (sbn.notification.extras.containsKey(Notification.EXTRA_MEDIA_SESSION)) {
activeController = null
emitMap(buildIdleMap())
}
}
private fun processNotification(sbn: StatusBarNotification) {
val extras = sbn.notification.extras
val tokenObj = extras.getParcelable<MediaSession.Token>(
Notification.EXTRA_MEDIA_SESSION
) ?: return
try {
val controller = MediaController(applicationContext, tokenObj)
val metadata = controller.metadata ?: return
val playbackState = controller.playbackState
// Keep a reference so play/pause commands can be sent
activeController = controller
val isPlaying = playbackState?.state == PlaybackState.STATE_PLAYING
val title = metadata.getString(MediaMetadata.METADATA_KEY_TITLE) ?: "Unknown Title"
val artist = metadata.getString(MediaMetadata.METADATA_KEY_ARTIST)
?: metadata.getString(MediaMetadata.METADATA_KEY_ALBUM_ARTIST)
?: "Unknown Artist"
val album = metadata.getString(MediaMetadata.METADATA_KEY_ALBUM) ?: ""
val albumArtBytes = metadata
.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART)
?.let { bitmapToBytes(it) }
?: metadata
.getBitmap(MediaMetadata.METADATA_KEY_ART)
?.let { bitmapToBytes(it) }
emitMap(mapOf(
"title" to title,
"artist" to artist,
"album" to album,
"albumArt" to albumArtBytes,
"isPlaying" to isPlaying
))
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun buildIdleMap(): Map<String, Any?> = mapOf(
"title" to "Nothing Playing", "artist" to "", "album" to "",
"albumArt" to null, "isPlaying" to false
)
private fun emitMap(map: Map<String, Any?>) {
lastMediaMap = map
val artBytes = map["albumArt"] as? ByteArray
val isPlaying = map["isPlaying"] as? Boolean ?: false
// Push to static widget
VinylWidgetProvider.pushUpdate(applicationContext, artBytes)
// Drive animated widget: update art + play state
VinylAnimatedWidgetService.instance?.updateArt(artBytes)
VinylAnimatedWidgetService.isPlaying = isPlaying
android.os.Handler(android.os.Looper.getMainLooper()).post {
eventSink?.success(map)
}
}
private fun bitmapToBytes(bitmap: Bitmap): ByteArray {
val scaled = if (bitmap.width > 512 || bitmap.height > 512)
Bitmap.createScaledBitmap(bitmap, 512, 512, true) else bitmap
return ByteArrayOutputStream().also { out ->
scaled.compress(Bitmap.CompressFormat.JPEG, 85, out)
}.toByteArray()
}
}
@@ -0,0 +1,119 @@
// android/app/src/main/kotlin/pt/ruifpb/openvinyl/MediaListenerService.kt
package pt.ruifpb.openvinyl
import android.app.Notification
import android.content.ComponentName
import android.content.Context
import android.graphics.Bitmap
import android.media.MediaMetadata
import android.media.session.MediaController
import android.media.session.MediaSession
import android.media.session.PlaybackState
import android.service.notification.NotificationListenerService
import android.service.notification.StatusBarNotification
import io.flutter.plugin.common.EventChannel
import java.io.ByteArrayOutputStream
class MediaListenerService : NotificationListenerService() {
companion object {
var eventSink: EventChannel.EventSink? = null
var lastMediaMap: Map<String, Any?>? = null
// Hold the active MediaController so MainActivity can send
// play/pause commands through it
var activeController: MediaController? = null
fun isEnabled(context: Context): Boolean {
val flat = android.provider.Settings.Secure.getString(
context.contentResolver,
"enabled_notification_listeners"
) ?: return false
val cn = ComponentName(context, MediaListenerService::class.java)
return flat.contains(cn.flattenToString())
}
// Called by MainActivity via the platform channel
fun sendPlayPause() {
val ctrl = activeController ?: return
val state = ctrl.playbackState?.state
if (state == PlaybackState.STATE_PLAYING) {
ctrl.transportControls.pause()
} else {
ctrl.transportControls.play()
}
}
}
override fun onNotificationPosted(sbn: StatusBarNotification) {
processNotification(sbn)
}
override fun onNotificationRemoved(sbn: StatusBarNotification) {
if (sbn.notification.extras.containsKey(Notification.EXTRA_MEDIA_SESSION)) {
activeController = null
emitMap(buildIdleMap())
}
}
private fun processNotification(sbn: StatusBarNotification) {
val extras = sbn.notification.extras
val tokenObj = extras.getParcelable<MediaSession.Token>(
Notification.EXTRA_MEDIA_SESSION
) ?: return
try {
val controller = MediaController(applicationContext, tokenObj)
val metadata = controller.metadata ?: return
val playbackState = controller.playbackState
// Keep a reference so play/pause commands can be sent
activeController = controller
val isPlaying = playbackState?.state == PlaybackState.STATE_PLAYING
val title = metadata.getString(MediaMetadata.METADATA_KEY_TITLE) ?: "Unknown Title"
val artist = metadata.getString(MediaMetadata.METADATA_KEY_ARTIST)
?: metadata.getString(MediaMetadata.METADATA_KEY_ALBUM_ARTIST)
?: "Unknown Artist"
val album = metadata.getString(MediaMetadata.METADATA_KEY_ALBUM) ?: ""
val albumArtBytes = metadata
.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART)
?.let { bitmapToBytes(it) }
?: metadata
.getBitmap(MediaMetadata.METADATA_KEY_ART)
?.let { bitmapToBytes(it) }
emitMap(mapOf(
"title" to title,
"artist" to artist,
"album" to album,
"albumArt" to albumArtBytes,
"isPlaying" to isPlaying
))
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun buildIdleMap(): Map<String, Any?> = mapOf(
"title" to "Nothing Playing", "artist" to "", "album" to "",
"albumArt" to null, "isPlaying" to false
)
private fun emitMap(map: Map<String, Any?>) {
lastMediaMap = map
android.os.Handler(android.os.Looper.getMainLooper()).post {
eventSink?.success(map)
}
}
private fun bitmapToBytes(bitmap: Bitmap): ByteArray {
val scaled = if (bitmap.width > 512 || bitmap.height > 512)
Bitmap.createScaledBitmap(bitmap, 512, 512, true) else bitmap
return ByteArrayOutputStream().also { out ->
scaled.compress(Bitmap.CompressFormat.JPEG, 85, out)
}.toByteArray()
}
}
@@ -0,0 +1,33 @@
// android/app/src/main/kotlin/pt/ruifpb/openvinyl/VinylAnimatedWidgetProvider.kt
//
// AppWidgetProvider for the animated 2x2 vinyl widget.
// All it does is start/stop VinylAnimatedWidgetService — the service
// owns the frame loop and pushes frames itself.
package pt.ruifpb.openvinyl
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.Context
class VinylAnimatedWidgetProvider : AppWidgetProvider() {
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
// Widget added or screen rebooted — start the service
VinylAnimatedWidgetService.start(context)
}
override fun onEnabled(context: Context) {
// First animated widget placed on screen
VinylAnimatedWidgetService.start(context)
}
override fun onDisabled(context: Context) {
// Last animated widget removed — stop the service entirely
VinylAnimatedWidgetService.stop(context)
}
}
@@ -0,0 +1,196 @@
// android/app/src/main/kotlin/pt/ruifpb/openvinyl/VinylAnimatedWidgetService.kt
//
// Foreground Service that drives the spinning animation for the animated widget.
//
// Design:
// - Starts when the animated widget exists AND music is playing.
// - Stops (but doesn't destroy itself) when music pauses — loop idles.
// - Fully stops when no animated widgets are on screen.
// - Runs at 12fps — smooth enough for a vinyl spin, easy on battery.
// - Each tick rotates the pre-rendered base disc bitmap by a fixed
// angle delta and pushes it to VinylAnimatedWidgetProvider via
// AppWidgetManager.updateAppWidget().
//
// Frame rendering is intentionally lightweight:
// - The grooves/sheen/label are pre-composed once into a base bitmap.
// - Each frame only applies a Canvas rotation transform — no full redraw.
package pt.ruifpb.openvinyl
import android.app.*
import android.appwidget.AppWidgetManager
import android.content.*
import android.graphics.*
import android.os.*
import android.widget.RemoteViews
import androidx.core.app.NotificationCompat
class VinylAnimatedWidgetService : Service() {
companion object {
private const val CHANNEL_ID = "vinyl_widget_anim"
private const val NOTIF_ID = 42
private const val FPS = 12
private const val FRAME_MS = 1000L / FPS
// Degrees per frame for 33⅓ RPM feel at 12fps
// 33.33 RPM = 0.555 RPS = 200ms/rotation → 360/200ms * 83ms/frame ≈ 2°/frame
private const val DEG_PER_FRAME = 2f
private const val BITMAP_SIZE = 300
var isPlaying: Boolean = false
set(value) {
field = value
instance?.onPlayStateChanged()
}
internal var instance: VinylAnimatedWidgetService? = null
fun start(context: Context) {
val intent = Intent(context, VinylAnimatedWidgetService::class.java)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(intent)
} else {
context.startService(intent)
}
}
fun stop(context: Context) {
context.stopService(Intent(context, VinylAnimatedWidgetService::class.java))
}
}
private val handler = Handler(Looper.getMainLooper())
private var baseBitmap: Bitmap? = null // pre-rendered disc (no rotation)
private var currentAngle = 0f
private var running = false
private val frameRunnable = object : Runnable {
override fun run() {
if (!isPlaying) return // music paused — stop ticking, keep service alive
tick()
handler.postDelayed(this, FRAME_MS)
}
}
// ── Lifecycle ─────────────────────────────────────────────────────────────
override fun onCreate() {
super.onCreate()
instance = this
startForegroundWithNotification()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
// Rebuild base bitmap in case art changed since last start
val prefs = getSharedPreferences("VinylWidgetPrefs", MODE_PRIVATE)
val artB64 = prefs.getString("albumArtB64", null)
val artBytes = artB64?.let { android.util.Base64.decode(it, android.util.Base64.DEFAULT) }
baseBitmap = VinylWidgetRenderer.render(this, BITMAP_SIZE, artBytes)
if (isPlaying) startLoop()
return START_STICKY
}
override fun onDestroy() {
instance = null
stopLoop()
super.onDestroy()
}
override fun onBind(intent: Intent?): IBinder? = null
// ── Called by companion when play state or art changes ────────────────────
fun onPlayStateChanged() {
if (isPlaying) startLoop() else stopLoop()
}
fun updateArt(artBytes: ByteArray?) {
baseBitmap = VinylWidgetRenderer.render(this, BITMAP_SIZE, artBytes)
currentAngle = 0f
}
// ── Animation loop ────────────────────────────────────────────────────────
private fun startLoop() {
if (running) return
running = true
handler.post(frameRunnable)
}
private fun stopLoop() {
running = false
handler.removeCallbacks(frameRunnable)
}
private fun tick() {
currentAngle = (currentAngle + DEG_PER_FRAME) % 360f
val base = baseBitmap ?: return
pushFrame(rotateBitmap(base, currentAngle))
}
// ── Bitmap rotation ───────────────────────────────────────────────────────
private fun rotateBitmap(src: Bitmap, degrees: Float): Bitmap {
val size = src.width
val result = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
val canvas = Canvas(result)
val matrix = Matrix().apply { postRotate(degrees, size / 2f, size / 2f) }
canvas.drawBitmap(src, matrix, Paint(Paint.ANTI_ALIAS_FLAG))
return result
}
// ── Push frame to all animated widgets ───────────────────────────────────
private fun pushFrame(bitmap: Bitmap) {
val manager = AppWidgetManager.getInstance(this)
val component = ComponentName(this, VinylAnimatedWidgetProvider::class.java)
val ids = manager.getAppWidgetIds(component)
if (ids.isEmpty()) {
// No animated widgets on screen — shut down to save battery
stopSelf()
return
}
val launchIntent = Intent(this, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
}
val pendingIntent = PendingIntent.getActivity(
this, 0, launchIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
val views = RemoteViews(packageName, R.layout.vinyl_widget_layout).apply {
setImageViewBitmap(R.id.widget_vinyl_image, bitmap)
setOnClickPendingIntent(R.id.widget_vinyl_image, pendingIntent)
}
for (id in ids) manager.updateAppWidget(id, views)
}
// ── Foreground notification (required for foreground service) ─────────────
private fun startForegroundWithNotification() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
CHANNEL_ID,
"Vinyl Widget Animation",
NotificationManager.IMPORTANCE_MIN // silent, no sound/vibration
).apply {
description = "Keeps the animated vinyl widget spinning"
setShowBadge(false)
}
getSystemService(NotificationManager::class.java)
.createNotificationChannel(channel)
}
val notif = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("OpenVinyl")
.setContentText("Animated widget active")
.setSmallIcon(android.R.drawable.ic_media_play)
.setPriority(NotificationCompat.PRIORITY_MIN)
.setSilent(true)
.build()
startForeground(NOTIF_ID, notif)
}
}
@@ -0,0 +1,114 @@
// android/app/src/main/kotlin/pt/ruifpb/openvinyl/VinylWidgetProvider.kt
//
// AppWidgetProvider for the 2x2 vinyl home screen widget.
//
// Data flow:
// 1. MediaListenerService detects a track change and calls
// VinylWidgetProvider.pushUpdate() with the latest album art bytes.
// 2. pushUpdate() stores the bytes in SharedPreferences and calls
// AppWidgetManager.updateAppWidget() directly — no polling needed.
// 3. onUpdate() is also called by Android on reboot/add-to-screen,
// and reads from SharedPreferences to restore the last known state.
package pt.ruifpb.openvinyl
import android.app.PendingIntent
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.util.Base64
import android.widget.RemoteViews
import android.os.Handler
import android.os.Looper
class VinylWidgetProvider : AppWidgetProvider() {
companion object {
private const val PREFS_NAME = "VinylWidgetPrefs"
private const val KEY_ART = "albumArtB64"
/**
* Called by MediaListenerService whenever the track or art changes.
* Stores art in SharedPreferences then forces a widget redraw.
*/
fun pushUpdate(context: Context, albumArtBytes: ByteArray?) {
// Store art bytes as Base64 in SharedPreferences
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
prefs.edit().apply {
if (albumArtBytes != null) {
putString(KEY_ART, Base64.encodeToString(albumArtBytes, Base64.DEFAULT))
} else {
remove(KEY_ART)
}
apply()
}
// Force redraw on main thread
Handler(Looper.getMainLooper()).post {
val manager = AppWidgetManager.getInstance(context)
val component = ComponentName(context, VinylWidgetProvider::class.java)
val widgetIds = manager.getAppWidgetIds(component)
if (widgetIds.isNotEmpty()) {
updateWidgets(context, manager, widgetIds, albumArtBytes)
}
}
}
private fun updateWidgets(
context: Context,
manager: AppWidgetManager,
widgetIds: IntArray,
albumArtBytes: ByteArray?
) {
// Determine a reasonable pixel size for the bitmap.
// 300px is plenty for a 2x2 widget on any density.
val size = 300
val bitmap = VinylWidgetRenderer.render(context, size, albumArtBytes)
// Tap widget → open app
val launchIntent = Intent(context, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
}
val pendingIntent = PendingIntent.getActivity(
context, 0, launchIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
val views = RemoteViews(context.packageName, R.layout.vinyl_widget_layout).apply {
setImageViewBitmap(R.id.widget_vinyl_image, bitmap)
setOnClickPendingIntent(R.id.widget_vinyl_image, pendingIntent)
}
for (id in widgetIds) {
manager.updateAppWidget(id, views)
}
}
}
// ── AppWidgetProvider callbacks ───────────────────────────────────────────
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
// Restore last known album art from prefs (e.g. after reboot)
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
val artB64 = prefs.getString(KEY_ART, null)
val artBytes = artB64?.let { Base64.decode(it, Base64.DEFAULT) }
updateWidgets(context, appWidgetManager, appWidgetIds, artBytes)
}
override fun onDeleted(context: Context, appWidgetIds: IntArray) {
// Nothing to clean up per-instance
}
override fun onDisabled(context: Context) {
// Last widget removed — clear stored art to free memory
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
.edit().clear().apply()
}
}
@@ -0,0 +1,158 @@
// android/app/src/main/kotlin/pt/ruifpb/openvinyl/VinylWidgetRenderer.kt
//
// Draws a vinyl disc bitmap entirely in Canvas — no View inflation needed.
// Called by VinylWidgetProvider to produce the ImageView bitmap.
//
// The disc has:
// - Dark base circle
// - Concentric groove rings
// - Radial edge fade (matches the in-app vinyl look)
// - Centre label circle filled with album art (or a music note if none)
// - Subtle sheen highlight
package pt.ruifpb.openvinyl
import android.content.Context
import android.graphics.*
import android.graphics.drawable.BitmapDrawable
import kotlin.math.min
object VinylWidgetRenderer {
/**
* Renders a vinyl disc into a [size]x[size] Bitmap.
* [albumArtBytes] is JPEG/PNG bytes, or null for a placeholder.
*/
fun render(context: Context, size: Int, albumArtBytes: ByteArray?): Bitmap {
val bmp = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bmp)
val cx = size / 2f
val cy = size / 2f
val radius = size / 2f - 4f // slight inset so edge isn't clipped
drawDisc(canvas, cx, cy, radius)
drawGrooves(canvas, cx, cy, radius)
drawEdgeFade(canvas, cx, cy, radius)
drawSheen(canvas, cx, cy, radius)
drawLabel(canvas, cx, cy, radius, albumArtBytes)
return bmp
}
// ── Disc base ─────────────────────────────────────────────────────────────
private fun drawDisc(canvas: Canvas, cx: Float, cy: Float, r: Float) {
canvas.drawCircle(cx, cy, r, Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = 0xFF0D0D0D.toInt()
})
}
// ── Groove rings ──────────────────────────────────────────────────────────
private fun drawGrooves(canvas: Canvas, cx: Float, cy: Float, r: Float) {
val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.STROKE
strokeWidth = 2.2f
}
val innerEdge = r * 0.55f
val outerEdge = r * 0.94f
val grooveCount = 10
val step = (outerEdge - innerEdge) / grooveCount
for (i in 0 until grooveCount) {
val gr = innerEdge + i * step
paint.color = if (i % 2 == 0) 0xFF323232.toInt() else 0xFF1A1A1A.toInt()
canvas.drawCircle(cx, cy, gr, paint)
}
}
// ── Radial edge fade — blends disc into transparent background ────────────
private fun drawEdgeFade(canvas: Canvas, cx: Float, cy: Float, r: Float) {
val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
shader = RadialGradient(
cx, cy, r,
intArrayOf(
Color.TRANSPARENT,
Color.TRANSPARENT,
0x73000000, // ~45% black at 92% radius
0xD9000000.toInt() // ~85% black at rim
),
floatArrayOf(0f, 0.78f, 0.92f, 1.0f),
Shader.TileMode.CLAMP
)
}
canvas.drawCircle(cx, cy, r, paint)
}
// ── Highlight sheen ───────────────────────────────────────────────────────
private fun drawSheen(canvas: Canvas, cx: Float, cy: Float, r: Float) {
val offsetX = cx - r * 0.35f
val offsetY = cy - r * 0.35f
val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
shader = RadialGradient(
offsetX, offsetY, r * 0.85f,
intArrayOf(0x12FFFFFF, Color.TRANSPARENT),
null,
Shader.TileMode.CLAMP
)
}
canvas.drawCircle(cx, cy, r, paint)
}
// ── Centre label with album art ───────────────────────────────────────────
private fun drawLabel(
canvas: Canvas,
cx: Float, cy: Float, r: Float,
albumArtBytes: ByteArray?
) {
val labelR = r * 0.52f
// Dark label background
canvas.drawCircle(cx, cy, labelR, Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = 0xFF1A1A1A.toInt()
})
if (albumArtBytes != null) {
// Decode and crop album art into a circle
val raw = BitmapFactory.decodeByteArray(albumArtBytes, 0, albumArtBytes.size)
if (raw != null) {
val side = (labelR * 2).toInt()
val scaled = Bitmap.createScaledBitmap(raw, side, side, true)
val circleBmp = Bitmap.createBitmap(side, side, Bitmap.Config.ARGB_8888)
val c2 = Canvas(circleBmp)
val p = Paint(Paint.ANTI_ALIAS_FLAG)
val rect = RectF(0f, 0f, side.toFloat(), side.toFloat())
c2.drawOval(rect, p)
p.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN)
c2.drawBitmap(scaled, 0f, 0f, p)
canvas.drawBitmap(
circleBmp,
cx - labelR,
cy - labelR,
Paint(Paint.ANTI_ALIAS_FLAG)
)
}
} else {
// Placeholder: simple music note text
canvas.drawText(
"",
cx,
cy + 14f,
Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = 0x66FFFFFF
textSize = labelR * 0.7f
textAlign = Paint.Align.CENTER
}
)
}
// Thin centre spindle hole
canvas.drawCircle(cx, cy, r * 0.025f, Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = 0xFF333333.toInt()
})
}
}
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- android/app/src/main/res/layout/vinyl_widget_layout.xml -->
<!-- -->
<!-- Layout for the 2x2 vinyl widget. Structure: -->
<!-- FrameLayout (root, square) -->
<!-- └─ ImageView: the vinyl disc bitmap drawn by VinylWidgetView -->
<!-- (filled by VinylWidgetProvider at update time) -->
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="8dp">
<ImageView
android:id="@+id/widget_vinyl_image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:scaleType="fitCenter"
android:contentDescription="Now playing vinyl" />
</FrameLayout>
Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- android/app/src/main/res/xml/notification_listener_service.xml -->
<!-- Declares which notification categories the service can access. -->
<!-- An empty <notification-listener-service> tag is sufficient; -->
<!-- Android uses it to show the app in Notification Listener settings.-->
<notification-listener-service
xmlns:android="http://schemas.android.com/apk/res/android" />
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- android/app/src/main/res/xml/vinyl_widget_animated_info.xml -->
<appwidget-provider
xmlns:android="http://schemas.android.com/apk/res/android"
android:minWidth="146dp"
android:minHeight="146dp"
android:targetCellWidth="2"
android:targetCellHeight="2"
android:updatePeriodMillis="0"
android:initialLayout="@layout/vinyl_widget_layout"
android:widgetCategory="home_screen"
android:resizeMode="none" />
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider
xmlns:android="http://schemas.android.com/apk/res/android"
android:minWidth="146dp"
android:minHeight="146dp"
android:targetCellWidth="2"
android:targetCellHeight="2"
android:updatePeriodMillis="0"
android:initialLayout="@layout/vinyl_widget_layout"
android:widgetCategory="home_screen"
android:resizeMode="none" />
@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
+24
View File
@@ -0,0 +1,24 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
val newBuildDir: Directory =
rootProject.layout.buildDirectory
.dir("../../build")
.get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}
+2
View File
@@ -0,0 +1,2 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
+5
View File
@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip
+26
View File
@@ -0,0 +1,26 @@
pluginManagement {
val flutterSdkPath =
run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.11.1" apply false
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
}
include(":app")
+34
View File
@@ -0,0 +1,34 @@
**/dgph
*.mode1v3
*.mode2v3
*.moved-aside
*.pbxuser
*.perspectivev3
**/*sync/
.sconsign.dblite
.tags*
**/.vagrant/
**/DerivedData/
Icon?
**/Pods/
**/.symlinks/
profile
xcuserdata
**/.generated/
Flutter/App.framework
Flutter/Flutter.framework
Flutter/Flutter.podspec
Flutter/Generated.xcconfig
Flutter/ephemeral/
Flutter/app.flx
Flutter/app.zip
Flutter/flutter_assets/
Flutter/flutter_export_environment.sh
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!default.mode1v3
!default.mode2v3
!default.pbxuser
!default.perspectivev3
+24
View File
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
</dict>
</plist>
+1
View File
@@ -0,0 +1 @@
#include "Generated.xcconfig"
+1
View File
@@ -0,0 +1 @@
#include "Generated.xcconfig"
+620
View File
@@ -0,0 +1,620 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
remoteInfo = Runner;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C807B294A618700263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C8080294A63A400263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
);
buildRules = (
);
dependencies = (
331C8086294A63A400263BE5 /* PBXTargetDependency */,
);
name = RunnerTests;
productName = RunnerTests;
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C8080294A63A400263BE5 = {
CreatedOnToolsVersion = 14.0;
TestTargetID = 97C146ED1CF9000F007C117D;
};
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
331C8080294A63A400263BE5 /* RunnerTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
331C807F294A63A400263BE5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
331C807D294A63A400263BE5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 97C146ED1CF9000F007C117D /* Runner */;
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C146FB1CF9000F007C117D /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Profile;
};
249021D4217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.openVinyl;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Profile;
};
331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.openVinyl.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Debug;
};
331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.openVinyl.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Release;
};
331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.openVinyl.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
97C147061CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.openVinyl;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
97C147071CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.openVinyl;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
331C8088294A63A400263BE5 /* Debug */,
331C8089294A63A400263BE5 /* Release */,
331C808A294A63A400263BE5 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147031CF9000F007C117D /* Debug */,
97C147041CF9000F007C117D /* Release */,
249021D3217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147061CF9000F007C117D /* Debug */,
97C147071CF9000F007C117D /* Release */,
249021D4217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>
@@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C8080294A63A400263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>
+16
View File
@@ -0,0 +1,16 @@
import Flutter
import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
}
}
@@ -0,0 +1,122 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "LaunchImage.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

@@ -0,0 +1,5 @@
# Launch Screen Assets
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="168" height="185"/>
</resources>
</document>
+26
View File
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>
+70
View File
@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Open Vinyl</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>open_vinyl</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneClassName</key>
<string>UIWindowScene</string>
<key>UISceneConfigurationName</key>
<string>flutter</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>
+1
View File
@@ -0,0 +1 @@
#import "GeneratedPluginRegistrant.h"
+6
View File
@@ -0,0 +1,6 @@
import Flutter
import UIKit
class SceneDelegate: FlutterSceneDelegate {
}
+12
View File
@@ -0,0 +1,12 @@
import Flutter
import UIKit
import XCTest
class RunnerTests: XCTestCase {
func testExample() {
// If you add code to the Runner application, consider adding tests here.
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
}
}
+89
View File
@@ -0,0 +1,89 @@
// lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'services/media_service.dart';
import 'screens/player_screen.dart';
import 'screens/permission_screen.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
// Force portrait
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
runApp(const OpenVinylApp());
}
class OpenVinylApp extends StatelessWidget {
const OpenVinylApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'OpenVinyl',
debugShowCheckedModeBanner: false,
theme: ThemeData.dark(useMaterial3: true),
home: AppEntry(mediaService: MediaService()),
);
}
}
/// Checks notification permission on startup, shows the right screen.
class AppEntry extends StatefulWidget {
final MediaService mediaService;
const AppEntry({super.key, required this.mediaService});
@override
State<AppEntry> createState() => _AppEntryState();
}
class _AppEntryState extends State<AppEntry> with WidgetsBindingObserver {
bool? _hasPermission; // null = loading
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_checkPermission();
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
// Re-check permission when the user returns from system settings
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed && _hasPermission == false) {
_checkPermission();
}
}
Future<void> _checkPermission() async {
final granted = await widget.mediaService.hasPermission();
setState(() => _hasPermission = granted);
}
@override
Widget build(BuildContext context) {
if (_hasPermission == null) {
// Still loading
return const Scaffold(
backgroundColor: Color(0xFF0D0D0D),
body: Center(
child: CircularProgressIndicator(color: Colors.white30),
),
);
}
if (!_hasPermission!) {
return PermissionScreen(
mediaService: widget.mediaService,
onPermissionGranted: () => setState(() => _hasPermission = true),
);
}
return PlayerScreen(mediaService: widget.mediaService);
}
}
+49
View File
@@ -0,0 +1,49 @@
// lib/models/media_info.dart
//
// Represents the currently playing track's metadata.
// Populated by NotificationListenerService via platform channel.
import 'dart:typed_data';
class MediaInfo {
final String title;
final String artist;
final String album;
final Uint8List? albumArt; // Raw bytes from notification
final bool isPlaying;
const MediaInfo({
required this.title,
required this.artist,
required this.album,
this.albumArt,
required this.isPlaying,
});
// Empty/idle state
static const empty = MediaInfo(
title: 'Nothing Playing',
artist: '',
album: '',
isPlaying: false,
);
MediaInfo copyWith({
String? title,
String? artist,
String? album,
Uint8List? albumArt,
bool? isPlaying,
}) {
return MediaInfo(
title: title ?? this.title,
artist: artist ?? this.artist,
album: album ?? this.album,
albumArt: albumArt ?? this.albumArt,
isPlaying: isPlaying ?? this.isPlaying,
);
}
@override
String toString() => 'MediaInfo(title: $title, artist: $artist, isPlaying: $isPlaying)';
}
+88
View File
@@ -0,0 +1,88 @@
// lib/screens/permission_screen.dart
//
// Shown when the user hasn't yet granted Notification Listener access.
// Explains why it's needed and opens the system settings page.
import 'package:flutter/material.dart';
import '../services/media_service.dart';
class PermissionScreen extends StatelessWidget {
final MediaService mediaService;
final VoidCallback onPermissionGranted;
const PermissionScreen({
super.key,
required this.mediaService,
required this.onPermissionGranted,
});
Future<void> _openSettings(BuildContext context) async {
await mediaService.requestPermission();
// Poll after a short delay — the user may have just come back from settings
await Future.delayed(const Duration(seconds: 1));
final granted = await mediaService.hasPermission();
if (granted) onPermissionGranted();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF0D0D0D),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(36),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.notifications_active_outlined,
color: Colors.white54,
size: 72,
),
const SizedBox(height: 32),
const Text(
'Notification Access Needed',
style: TextStyle(
color: Colors.white,
fontSize: 22,
fontWeight: FontWeight.w700,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
Text(
'OpenVinyl reads your currently playing music from your notification shade — '
'it never accesses your messages or other notifications.\n\n'
'Tap below to open system settings and enable access for OpenVinyl.',
style: TextStyle(
color: Colors.white.withOpacity(0.6),
fontSize: 15,
height: 1.55,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 40),
FilledButton.icon(
onPressed: () => _openSettings(context),
icon: const Icon(Icons.settings_outlined),
label: const Text('Open Settings'),
style: FilledButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: Colors.black,
padding: const EdgeInsets.symmetric(
horizontal: 32,
vertical: 14,
),
textStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
],
),
),
),
);
}
}
+407
View File
@@ -0,0 +1,407 @@
// lib/screens/player_screen.dart
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:palette_generator/palette_generator.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
import '../models/media_info.dart';
import '../services/media_service.dart';
import '../widgets/vinyl_record.dart';
import '../widgets/tonearm.dart';
class PlayerScreen extends StatefulWidget {
final MediaService mediaService;
const PlayerScreen({super.key, required this.mediaService});
@override
State<PlayerScreen> createState() => _PlayerScreenState();
}
class _PlayerScreenState extends State<PlayerScreen> {
MediaInfo _current = MediaInfo.empty;
Color _bgTop = const Color(0xFF1A1A2E);
Color _bgBottom = const Color(0xFF0D0D0D);
bool _wakeLockOn = false;
Timer? _paletteDebounce;
@override
void initState() {
super.initState();
_loadInitial();
widget.mediaService.mediaStream.listen(_onMediaUpdate);
}
@override
void dispose() {
_paletteDebounce?.cancel();
WakelockPlus.disable();
super.dispose();
}
Future<void> _loadInitial() async {
final info = await widget.mediaService.getCurrentMedia();
_onMediaUpdate(info);
}
void _onMediaUpdate(MediaInfo info) {
setState(() => _current = info);
_paletteDebounce?.cancel();
_paletteDebounce = Timer(
const Duration(milliseconds: 300),
() => _updatePalette(info),
);
}
Future<void> _updatePalette(MediaInfo info) async {
if (info.albumArt == null) {
if (mounted) setState(() {
_bgTop = const Color(0xFF1A1A2E);
_bgBottom = const Color(0xFF0D0D0D);
});
return;
}
try {
final generator = await PaletteGenerator.fromImageProvider(
MemoryImage(info.albumArt!),
maximumColorCount: 8,
);
if (!mounted) return;
final dominant = generator.dominantColor?.color ?? const Color(0xFF1A1A2E);
final muted = generator.mutedColor?.color ?? const Color(0xFF0D0D0D);
setState(() {
_bgTop = _darken(dominant, 0.45);
_bgBottom = _darken(muted, 0.7);
});
} catch (_) {}
}
void _toggleWakeLock() {
final next = !_wakeLockOn;
setState(() => _wakeLockOn = next);
WakelockPlus.toggle(enable: next);
}
Color _darken(Color c, double amount) {
final hsl = HSLColor.fromColor(c);
return hsl.withLightness((hsl.lightness * (1 - amount)).clamp(0.0, 1.0)).toColor();
}
@override
Widget build(BuildContext context) {
final isLandscape =
MediaQuery.orientationOf(context) == Orientation.landscape;
return Scaffold(
body: AnimatedContainer(
duration: const Duration(milliseconds: 800),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [_bgTop, _bgBottom],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: SafeArea(
child: isLandscape
? _LandscapeLayout(
info: _current,
mediaService: widget.mediaService,
wakeLockOn: _wakeLockOn,
onWakeLockTap: _toggleWakeLock,
)
: _PortraitLayout(
info: _current,
mediaService: widget.mediaService,
wakeLockOn: _wakeLockOn,
onWakeLockTap: _toggleWakeLock,
),
),
),
);
}
}
// ─── Portrait layout (original behaviour) ────────────────────────────────────
class _PortraitLayout extends StatelessWidget {
final MediaInfo info;
final MediaService mediaService;
final bool wakeLockOn;
final VoidCallback onWakeLockTap;
const _PortraitLayout({
required this.info,
required this.mediaService,
required this.wakeLockOn,
required this.onWakeLockTap,
});
@override
Widget build(BuildContext context) {
return Stack(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Flexible(flex: 2, child: SizedBox(height: 32)),
_TrackInfo(info: info),
const Flexible(flex: 2, child: SizedBox(height: 32)),
_TurntableStage(info: info, mediaService: mediaService),
const Flexible(flex: 3, child: SizedBox(height: 48)),
],
),
Positioned(
left: 20,
bottom: 20,
child: _WakeLockButton(isOn: wakeLockOn, onTap: onWakeLockTap),
),
],
);
}
}
// ─── Landscape layout — vinyl centred, track info above ──────────────────────
//
// In landscape the screen is wide and short. We:
// - Centre the vinyl disc (no left-shift, it fits without clipping needed)
// - Size it to 90% of screen height so it fills the shorter dimension
// - Place track info in a compact row above the disc
// - Keep the wake lock button bottom-left
class _LandscapeLayout extends StatelessWidget {
final MediaInfo info;
final MediaService mediaService;
final bool wakeLockOn;
final VoidCallback onWakeLockTap;
const _LandscapeLayout({
required this.info,
required this.mediaService,
required this.wakeLockOn,
required this.onWakeLockTap,
});
@override
Widget build(BuildContext context) {
final size = MediaQuery.sizeOf(context);
final stageSize = (size.height * 0.82).clamp(0.0, size.width * 0.75);
return Stack(
children: [
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(height: 8),
_TrackInfo(info: info, centred: true),
const SizedBox(height: 12),
// Centred disc — no shift, tonearm sits within the stage
SizedBox(
width: size.width,
height: stageSize,
child: Stack(
clipBehavior: Clip.hardEdge,
children: [
// Record — centred
Positioned(
left: (size.width - stageSize) / 2,
top: 0,
width: stageSize,
height: stageSize,
child: Padding(
padding: EdgeInsets.all(stageSize * 0.04),
child: RepaintBoundary(
child: VinylRecord(
albumArt: info.albumArt,
isPlaying: info.isPlaying,
),
),
),
),
// Tonearm — full width so pivot position stays consistent
Positioned.fill(
child: Tonearm(
isPlaying: info.isPlaying,
onToggle: (_) => mediaService.playPause(),
),
),
],
),
),
],
),
Positioned(
left: 20,
bottom: 12,
child: _WakeLockButton(isOn: wakeLockOn, onTap: onWakeLockTap),
),
],
);
}
}
// ─── Track info ───────────────────────────────────────────────────────────────
class _TrackInfo extends StatelessWidget {
final MediaInfo info;
final bool centred;
const _TrackInfo({required this.info, this.centred = false});
@override
Widget build(BuildContext context) {
final screenW = MediaQuery.sizeOf(context).width;
final screenH = MediaQuery.sizeOf(context).height;
final ref = screenW.clamp(0.0, screenH * 0.5);
final titleSize = (ref * 0.072).clamp(16.0, 36.0);
final subSize = (ref * 0.050).clamp(12.0, 24.0);
final align = centred ? TextAlign.center : TextAlign.start;
final crossAxis = centred
? CrossAxisAlignment.center
: CrossAxisAlignment.start;
return Padding(
padding: centred
? const EdgeInsets.symmetric(horizontal: 24)
: const EdgeInsets.only(left: 24, right: 80),
child: Column(
crossAxisAlignment: crossAxis,
mainAxisSize: MainAxisSize.min,
children: [
AnimatedSwitcher(
duration: const Duration(milliseconds: 400),
child: Text(
info.title,
key: ValueKey(info.title),
textAlign: align,
style: TextStyle(
color: Colors.white,
fontSize: titleSize,
fontWeight: FontWeight.w700,
letterSpacing: 0.3,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(height: 4),
AnimatedSwitcher(
duration: const Duration(milliseconds: 400),
child: Text(
info.artist.isNotEmpty ? info.artist : ' ',
key: ValueKey(info.artist),
textAlign: align,
style: TextStyle(
color: Colors.white.withOpacity(0.65),
fontSize: subSize,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
);
}
}
// ─── Turntable stage (portrait) ───────────────────────────────────────────────
class _TurntableStage extends StatelessWidget {
final MediaInfo info;
final MediaService mediaService;
const _TurntableStage({required this.info, required this.mediaService});
static const _shiftFraction = 0.18;
@override
Widget build(BuildContext context) {
final size = MediaQuery.sizeOf(context);
final screenW = size.width;
final screenH = size.height;
final stageSize = screenW.clamp(0.0, screenH * 0.80);
final shift = stageSize * _shiftFraction;
return SizedBox(
width: screenW,
height: stageSize,
child: Stack(
clipBehavior: Clip.hardEdge,
children: [
Positioned(
left: -shift,
top: 0,
width: stageSize,
height: stageSize,
child: Padding(
padding: EdgeInsets.all(stageSize * 0.04),
child: RepaintBoundary(
child: VinylRecord(
albumArt: info.albumArt,
isPlaying: info.isPlaying,
),
),
),
),
Positioned.fill(
child: Tonearm(
isPlaying: info.isPlaying,
onToggle: (_) => mediaService.playPause(),
),
),
],
),
);
}
}
// ─── Wake lock button ─────────────────────────────────────────────────────────
class _WakeLockButton extends StatelessWidget {
final bool isOn;
final VoidCallback onTap;
const _WakeLockButton({required this.isOn, required this.onTap});
@override
Widget build(BuildContext context) {
return Tooltip(
message: isOn ? 'Tap to allow screen sleep' : 'Keep screen on',
child: GestureDetector(
onTap: onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 250),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: isOn
? Colors.white.withOpacity(0.15)
: Colors.white.withOpacity(0.06),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: isOn
? Colors.white.withOpacity(0.5)
: Colors.white.withOpacity(0.15),
width: 1,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
isOn ? Icons.lock_open_rounded : Icons.lock_clock_outlined,
color: isOn ? Colors.white : Colors.white38,
size: 16,
),
const SizedBox(width: 6),
Text(
'Screen on',
style: TextStyle(
color: isOn ? Colors.white : Colors.white38,
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
],
),
),
),
);
}
}
+99
View File
@@ -0,0 +1,99 @@
// lib/services/media_service.dart
import 'dart:typed_data';
import 'package:flutter/services.dart';
import '../models/media_info.dart';
// Uncomment when adding widgets:
// import 'package:home_widget/home_widget.dart';
class MediaService {
static const _methodChannel = MethodChannel('pt.ruifpb.openvinyl/media_control');
static const _eventChannel = EventChannel('pt.ruifpb.openvinyl/media_events');
Stream<MediaInfo>? _mediaStream;
Stream<MediaInfo> get mediaStream {
_mediaStream ??= _eventChannel
.receiveBroadcastStream()
.map(_parseEvent)
.where((info) => info != null)
.cast<MediaInfo>();
return _mediaStream!;
}
Future<bool> hasPermission() async {
try {
return await _methodChannel.invokeMethod<bool>('hasPermission') ?? false;
} on PlatformException catch (e) {
print('MediaService.hasPermission error: $e');
return false;
}
}
Future<void> requestPermission() async {
try {
await _methodChannel.invokeMethod('requestPermission');
} on PlatformException catch (e) {
print('MediaService.requestPermission error: $e');
}
}
Future<MediaInfo> getCurrentMedia() async {
try {
final data = await _methodChannel.invokeMethod<Map>('getCurrentMedia');
if (data == null) return MediaInfo.empty;
return _parseMap(Map<String, dynamic>.from(data));
} on PlatformException catch (e) {
print('MediaService.getCurrentMedia error: $e');
return MediaInfo.empty;
}
}
/// Sends a play or pause command to the active MediaSession.
/// Android decides which based on the current playback state,
/// so this is a true toggle.
Future<void> playPause() async {
try {
await _methodChannel.invokeMethod('playPause');
} on PlatformException catch (e) {
print('MediaService.playPause error: $e');
}
}
// ─── Private helpers ──────────────────────────────────────────────────────
MediaInfo? _parseEvent(dynamic event) {
if (event == null) return null;
try {
final map = Map<String, dynamic>.from(event as Map);
final info = _parseMap(map);
// _pushToWidget(info); // uncomment when adding widgets
return info;
} catch (e) {
print('MediaService._parseEvent error: $e');
return null;
}
}
MediaInfo _parseMap(Map<String, dynamic> map) {
Uint8List? albumArt;
if (map['albumArt'] != null) {
albumArt = Uint8List.fromList(List<int>.from(map['albumArt']));
}
return MediaInfo(
title: map['title'] as String? ?? 'Unknown Title',
artist: map['artist'] as String? ?? 'Unknown Artist',
album: map['album'] as String? ?? '',
albumArt: albumArt,
isPlaying: map['isPlaying'] as bool? ?? false,
);
}
// ── WIDGET HOOK ────────────────────────────────────────────────────────────
// Future<void> _pushToWidget(MediaInfo info) async {
// await HomeWidget.saveWidgetData('title', info.title);
// await HomeWidget.saveWidgetData('artist', info.artist);
// await HomeWidget.updateWidget(androidName: 'VinylWidgetReceiver');
// }
}
+169
View File
@@ -0,0 +1,169 @@
// lib/widgets/tonearm.dart
//
// Tonearm that toggles play/pause on tap.
// Animates smoothly between lifted (paused) and on-record (playing) positions.
import 'dart:math' as math;
import 'package:flutter/material.dart';
class Tonearm extends StatefulWidget {
final bool isPlaying;
final ValueChanged<bool>? onToggle;
const Tonearm({super.key, required this.isPlaying, this.onToggle});
@override
State<Tonearm> createState() => _TonearmState();
}
class _TonearmState extends State<Tonearm>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
static const _restAngle = -0.38;
static const _playAngle = 0.05;
static const _angleRange = _playAngle - _restAngle;
double get _currentAngle => _restAngle + _controller.value * _angleRange;
Size _lastSize = const Size(300, 300);
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 600),
value: widget.isPlaying ? 1.0 : 0.0,
);
}
@override
void didUpdateWidget(Tonearm old) {
super.didUpdateWidget(old);
if (widget.isPlaying != old.isPlaying) {
widget.isPlaying ? _controller.forward() : _controller.reverse();
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
// ─── Hit test — tap anywhere near the arm body ────────────────────────────
bool _isNearArm(Offset touch) {
final unit = _lastSize.shortestSide;
final isLandscape = _lastSize.width > _lastSize.height;
final ref = isLandscape ? _lastSize.height : _lastSize.width;
final pivot = isLandscape
? Offset(_lastSize.width * 0.5 + ref * 0.42, _lastSize.height * 0.12)
: Offset(_lastSize.width * 0.8, _lastSize.height * 0.12);
final armLength = unit * 0.5;
final hitRadius = unit * 0.14;
final a = _currentAngle;
Offset toCanvas(double lx, double ly) {
final rx = lx * math.cos(a) - ly * math.sin(a);
final ry = lx * math.sin(a) + ly * math.cos(a);
return pivot + Offset(rx, ry);
}
final p0 = pivot;
final p1 = toCanvas(-armLength * 0.15, armLength * 0.75);
final p2 = toCanvas(-armLength * 0.22, armLength);
final p3 = toCanvas(-armLength * 0.28, armLength * 1.09);
return _distToSegment(touch, p0, p1) < hitRadius ||
_distToSegment(touch, p1, p2) < hitRadius ||
_distToSegment(touch, p2, p3) < hitRadius;
}
double _distToSegment(Offset p, Offset a, Offset b) {
final ab = b - a;
final ap = p - a;
final t = (ap.dx * ab.dx + ap.dy * ab.dy) /
(ab.dx * ab.dx + ab.dy * ab.dy + 1e-9);
return (p - (a + ab * t.clamp(0.0, 1.0))).distance;
}
void _onTap(Offset localPosition) {
if (!_isNearArm(localPosition)) return;
final next = !widget.isPlaying;
widget.onToggle?.call(next);
}
// ─── Build ────────────────────────────────────────────────────────────────
@override
Widget build(BuildContext context) {
return LayoutBuilder(builder: (context, constraints) {
_lastSize = Size(constraints.maxWidth, constraints.maxHeight);
return GestureDetector(
behavior: HitTestBehavior.translucent,
onTapUp: (d) => _onTap(d.localPosition),
child: AnimatedBuilder(
animation: _controller,
builder: (context, _) => CustomPaint(
painter: _TonearmPainter(angle: _currentAngle, size: _lastSize),
size: _lastSize,
),
),
);
});
}
}
// ─── Painter ─────────────────────────────────────────────────────────────────
class _TonearmPainter extends CustomPainter {
final double angle;
final Size size;
_TonearmPainter({required this.angle, required this.size});
@override
void paint(Canvas canvas, Size size) {
final unit = size.shortestSide;
final isLandscape = size.width > size.height;
final ref = isLandscape ? size.height : size.width;
final pivot = isLandscape
? Offset(size.width * 0.5 + ref * 0.42, size.height * 0.12)
: Offset(size.width * 0.8, size.height * 0.12);
final armLength = unit * 0.5;
final headshellExt = armLength * 0.09;
canvas.save();
canvas.translate(pivot.dx, pivot.dy);
canvas.rotate(angle);
final arm = Paint()
..strokeCap = StrokeCap.round
..style = PaintingStyle.stroke;
canvas.drawLine(Offset.zero,
Offset(-armLength * 0.15, armLength * 0.75),
arm..color = const Color(0xFFB8B8B8)..strokeWidth = unit * 0.012);
canvas.drawLine(
Offset(-armLength * 0.15, armLength * 0.75),
Offset(-armLength * 0.22, armLength),
arm..strokeWidth = unit * 0.009);
canvas.drawLine(
Offset(-armLength * 0.22, armLength),
Offset(-armLength * 0.28, armLength + headshellExt),
arm..color = const Color(0xFF888888)..strokeWidth = unit * 0.008);
canvas.drawCircle(
Offset(-armLength * 0.28, armLength + headshellExt * 1.2),
unit * 0.009,
Paint()..color = const Color(0xFFE0E0E0));
canvas.drawCircle(Offset.zero, unit * 0.022, Paint()..color = const Color(0xFF999999));
canvas.drawCircle(Offset.zero, unit * 0.013, Paint()..color = const Color(0xFF555555));
canvas.restore();
}
@override
bool shouldRepaint(_TonearmPainter old) =>
old.angle != angle || old.size != size;
}
+262
View File
@@ -0,0 +1,262 @@
// lib/widgets/vinyl_record.dart
//
// Jitter fixes applied:
//
// 1. The rotation Transform is now the ONLY thing AnimatedBuilder rebuilds.
// The disc and album art are passed as a static `child` — Flutter's
// AnimatedBuilder skips rebuilding the child subtree on every tick,
// so album art image decoding / layout never competes with the animation.
//
// 2. Album art crossfade is handled by AnimatedSwitcher inside the label,
// keyed on the art bytes identity — it fades smoothly without touching
// the rotation controller.
//
// 3. _VinylPainter is cached via RepaintBoundary so the groove canvas
// is only repainted when size changes, not on every rotation frame.
import 'dart:math' as math;
import 'dart:typed_data';
import 'package:flutter/material.dart';
class VinylRecord extends StatefulWidget {
final Uint8List? albumArt;
final bool isPlaying;
final Duration rotationDuration;
const VinylRecord({
super.key,
this.albumArt,
required this.isPlaying,
this.rotationDuration = const Duration(milliseconds: 1800),
});
@override
State<VinylRecord> createState() => _VinylRecordState();
}
class _VinylRecordState extends State<VinylRecord>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: widget.rotationDuration,
);
if (widget.isPlaying) _controller.repeat();
}
@override
void didUpdateWidget(VinylRecord old) {
super.didUpdateWidget(old);
if (widget.isPlaying && !_controller.isAnimating) {
_controller.repeat();
} else if (!widget.isPlaying && _controller.isAnimating) {
_controller
.animateTo(
1.0,
duration: const Duration(milliseconds: 600),
curve: Curves.easeOut,
)
.then((_) {
if (!widget.isPlaying) _controller.reset();
});
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final size = math.min(constraints.maxWidth, constraints.maxHeight);
// Build the static disc once and pass it as child —
// AnimatedBuilder will NOT call builder on this subtree each tick.
final disc = _VinylDisc(size: size, albumArt: widget.albumArt);
return AnimatedBuilder(
animation: _controller,
child: disc, // <-- built once, reused every frame
builder: (context, child) {
return Transform.rotate(
angle: _controller.value * 2 * math.pi,
child: child,
);
},
);
},
);
}
}
// ─── Static disc (grooves + label) ───────────────────────────────────────────
class _VinylDisc extends StatelessWidget {
final double size;
final Uint8List? albumArt;
const _VinylDisc({required this.size, this.albumArt});
@override
Widget build(BuildContext context) {
return SizedBox.square(
dimension: size,
child: Stack(
children: [
// Grooves — wrapped in RepaintBoundary so it is rasterised once
// and never redrawn unless size changes
Positioned.fill(
child: RepaintBoundary(
child: CustomPaint(painter: _VinylPainter()),
),
),
// Album art label in the centre
Center(
child: _AlbumArtLabel(size: size * 0.38, albumArt: albumArt),
),
],
),
);
}
}
// ─── Album art label with crossfade ──────────────────────────────────────────
class _AlbumArtLabel extends StatelessWidget {
final double size;
final Uint8List? albumArt;
const _AlbumArtLabel({required this.size, this.albumArt});
@override
Widget build(BuildContext context) {
return SizedBox.square(
dimension: size,
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 500),
child: _LabelCircle(
// Key on the bytes reference so switcher triggers on art change
key: ValueKey(albumArt?.hashCode),
size: size,
albumArt: albumArt,
),
),
);
}
}
class _LabelCircle extends StatelessWidget {
final double size;
final Uint8List? albumArt;
const _LabelCircle({super.key, required this.size, this.albumArt});
@override
Widget build(BuildContext context) {
return Container(
width: size,
height: size,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: const Color(0xFF1A1A1A),
image: albumArt != null
? DecorationImage(
image: MemoryImage(albumArt!),
fit: BoxFit.cover,
)
: null,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.4),
blurRadius: 8,
spreadRadius: 2,
),
],
),
child: albumArt == null
? const Center(
child: Icon(Icons.music_note, color: Colors.white38, size: 32),
)
: null,
);
}
}
// ─── Groove painter — shouldRepaint = false, wrapped in RepaintBoundary ───────
class _VinylPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final centre = Offset(size.width / 2, size.height / 2);
final radius = size.width / 2;
// Base disc
canvas.drawCircle(
centre,
radius,
Paint()..color = const Color(0xFF0D0D0D),
);
// Soft radial fade at the outer edge — blends into the background
// instead of leaving a hard ring
final edgeFade = Paint()
..shader = RadialGradient(
center: Alignment.center,
radius: 1.0,
colors: [
Colors.transparent,
Colors.transparent,
Colors.black.withOpacity(0.45),
Colors.black.withOpacity(0.85),
],
stops: const [0.0, 0.78, 0.92, 1.0],
).createShader(Rect.fromCircle(center: centre, radius: radius));
canvas.drawCircle(centre, radius, edgeFade);
final groovePaint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 0.6;
const grooveCount = 28;
final innerEdge = radius * 0.42;
final outerEdge = radius * 0.94;
final step = (outerEdge - innerEdge) / grooveCount;
for (int i = 0; i < grooveCount; i++) {
final r = innerEdge + i * step;
final brightness = (i % 2 == 0) ? 0x2A : 0x22;
groovePaint.color = Color.fromARGB(255, brightness, brightness, brightness);
canvas.drawCircle(centre, r, groovePaint);
}
// Sheen
final sheen = Paint()
..shader = RadialGradient(
center: const Alignment(-0.35, -0.35),
radius: 0.85,
colors: [Colors.white.withOpacity(0.07), Colors.transparent],
).createShader(Rect.fromCircle(center: centre, radius: radius));
canvas.drawCircle(centre, radius, sheen);
// Rim
canvas.drawCircle(
centre,
radius - 1,
Paint()
..color = Colors.white.withOpacity(0.08)
..style = PaintingStyle.stroke
..strokeWidth = 1.5,
);
}
@override
bool shouldRepaint(_VinylPainter old) => false;
}
+1
View File
@@ -0,0 +1 @@
flutter/ephemeral
+128
View File
@@ -0,0 +1,128 @@
# Project-level configuration.
cmake_minimum_required(VERSION 3.13)
project(runner LANGUAGES CXX)
# The name of the executable created for the application. Change this to change
# the on-disk name of your application.
set(BINARY_NAME "open_vinyl")
# The unique GTK application identifier for this application. See:
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
set(APPLICATION_ID "com.example.open_vinyl")
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
# versions of CMake.
cmake_policy(SET CMP0063 NEW)
# Load bundled libraries from the lib/ directory relative to the binary.
set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
# Root filesystem for cross-building.
if(FLUTTER_TARGET_PLATFORM_SYSROOT)
set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT})
set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
endif()
# Define build configuration options.
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE "Debug" CACHE
STRING "Flutter build mode" FORCE)
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
"Debug" "Profile" "Release")
endif()
# Compilation settings that should be applied to most targets.
#
# Be cautious about adding new options here, as plugins use this function by
# default. In most cases, you should add new options to specific targets instead
# of modifying this function.
function(APPLY_STANDARD_SETTINGS TARGET)
target_compile_features(${TARGET} PUBLIC cxx_std_14)
target_compile_options(${TARGET} PRIVATE -Wall -Werror)
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
endfunction()
# Flutter library and tool build rules.
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
add_subdirectory(${FLUTTER_MANAGED_DIR})
# System-level dependencies.
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
# Application build; see runner/CMakeLists.txt.
add_subdirectory("runner")
# Run the Flutter tool portions of the build. This must not be removed.
add_dependencies(${BINARY_NAME} flutter_assemble)
# Only the install-generated bundle's copy of the executable will launch
# correctly, since the resources must in the right relative locations. To avoid
# people trying to run the unbundled copy, put it in a subdirectory instead of
# the default top-level location.
set_target_properties(${BINARY_NAME}
PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run"
)
# Generated plugin build rules, which manage building the plugins and adding
# them to the application.
include(flutter/generated_plugins.cmake)
# === Installation ===
# By default, "installing" just makes a relocatable bundle in the build
# directory.
set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle")
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
endif()
# Start with a clean build bundle directory every time.
install(CODE "
file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\")
" COMPONENT Runtime)
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib")
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
COMPONENT Runtime)
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
COMPONENT Runtime)
install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES})
install(FILES "${bundled_library}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endforeach(bundled_library)
# Copy the native assets provided by the build.dart from all packages.
set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/")
install(DIRECTORY "${NATIVE_ASSETS_DIR}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
# Fully re-copy the assets directory on each build to avoid having stale files
# from a previous install.
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
install(CODE "
file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
" COMPONENT Runtime)
install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
# Install the AOT library on non-Debug builds only.
if(NOT CMAKE_BUILD_TYPE MATCHES "Debug")
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endif()
+88
View File
@@ -0,0 +1,88 @@
# This file controls Flutter-level build steps. It should not be edited.
cmake_minimum_required(VERSION 3.10)
set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
# Configuration provided via flutter tool.
include(${EPHEMERAL_DIR}/generated_config.cmake)
# TODO: Move the rest of this into files in ephemeral. See
# https://github.com/flutter/flutter/issues/57146.
# Serves the same purpose as list(TRANSFORM ... PREPEND ...),
# which isn't available in 3.10.
function(list_prepend LIST_NAME PREFIX)
set(NEW_LIST "")
foreach(element ${${LIST_NAME}})
list(APPEND NEW_LIST "${PREFIX}${element}")
endforeach(element)
set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE)
endfunction()
# === Flutter Library ===
# System-level dependencies.
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so")
# Published to parent scope for install step.
set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE)
list(APPEND FLUTTER_LIBRARY_HEADERS
"fl_basic_message_channel.h"
"fl_binary_codec.h"
"fl_binary_messenger.h"
"fl_dart_project.h"
"fl_engine.h"
"fl_json_message_codec.h"
"fl_json_method_codec.h"
"fl_message_codec.h"
"fl_method_call.h"
"fl_method_channel.h"
"fl_method_codec.h"
"fl_method_response.h"
"fl_plugin_registrar.h"
"fl_plugin_registry.h"
"fl_standard_message_codec.h"
"fl_standard_method_codec.h"
"fl_string_codec.h"
"fl_value.h"
"fl_view.h"
"flutter_linux.h"
)
list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/")
add_library(flutter INTERFACE)
target_include_directories(flutter INTERFACE
"${EPHEMERAL_DIR}"
)
target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}")
target_link_libraries(flutter INTERFACE
PkgConfig::GTK
PkgConfig::GLIB
PkgConfig::GIO
)
add_dependencies(flutter flutter_assemble)
# === Flutter tool backend ===
# _phony_ is a non-existent file to force this command to run every time,
# since currently there's no way to get a full input/output list from the
# flutter tool.
add_custom_command(
OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
${CMAKE_CURRENT_BINARY_DIR}/_phony_
COMMAND ${CMAKE_COMMAND} -E env
${FLUTTER_TOOL_ENVIRONMENT}
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh"
${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE}
VERBATIM
)
add_custom_target(flutter_assemble DEPENDS
"${FLUTTER_LIBRARY}"
${FLUTTER_LIBRARY_HEADERS}
)
@@ -0,0 +1,11 @@
//
// Generated file. Do not edit.
//
// clang-format off
#include "generated_plugin_registrant.h"
void fl_register_plugins(FlPluginRegistry* registry) {
}
@@ -0,0 +1,15 @@
//
// Generated file. Do not edit.
//
// clang-format off
#ifndef GENERATED_PLUGIN_REGISTRANT_
#define GENERATED_PLUGIN_REGISTRANT_
#include <flutter_linux/flutter_linux.h>
// Registers Flutter plugins.
void fl_register_plugins(FlPluginRegistry* registry);
#endif // GENERATED_PLUGIN_REGISTRANT_
+23
View File
@@ -0,0 +1,23 @@
#
# Generated file, do not edit.
#
list(APPEND FLUTTER_PLUGIN_LIST
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
)
set(PLUGIN_BUNDLED_LIBRARIES)
foreach(plugin ${FLUTTER_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin})
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
endforeach(plugin)
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin})
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
endforeach(ffi_plugin)
+26
View File
@@ -0,0 +1,26 @@
cmake_minimum_required(VERSION 3.13)
project(runner LANGUAGES CXX)
# Define the application target. To change its name, change BINARY_NAME in the
# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer
# work.
#
# Any new source files that you add to the application should be added here.
add_executable(${BINARY_NAME}
"main.cc"
"my_application.cc"
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
)
# Apply the standard set of build settings. This can be removed for applications
# that need different build settings.
apply_standard_settings(${BINARY_NAME})
# Add preprocessor definitions for the application ID.
add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}")
# Add dependency libraries. Add any application-specific dependencies here.
target_link_libraries(${BINARY_NAME} PRIVATE flutter)
target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
+6
View File
@@ -0,0 +1,6 @@
#include "my_application.h"
int main(int argc, char** argv) {
g_autoptr(MyApplication) app = my_application_new();
return g_application_run(G_APPLICATION(app), argc, argv);
}
+148
View File
@@ -0,0 +1,148 @@
#include "my_application.h"
#include <flutter_linux/flutter_linux.h>
#ifdef GDK_WINDOWING_X11
#include <gdk/gdkx.h>
#endif
#include "flutter/generated_plugin_registrant.h"
struct _MyApplication {
GtkApplication parent_instance;
char** dart_entrypoint_arguments;
};
G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
// Called when first Flutter frame received.
static void first_frame_cb(MyApplication* self, FlView* view) {
gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view)));
}
// Implements GApplication::activate.
static void my_application_activate(GApplication* application) {
MyApplication* self = MY_APPLICATION(application);
GtkWindow* window =
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
// Use a header bar when running in GNOME as this is the common style used
// by applications and is the setup most users will be using (e.g. Ubuntu
// desktop).
// If running on X and not using GNOME then just use a traditional title bar
// in case the window manager does more exotic layout, e.g. tiling.
// If running on Wayland assume the header bar will work (may need changing
// if future cases occur).
gboolean use_header_bar = TRUE;
#ifdef GDK_WINDOWING_X11
GdkScreen* screen = gtk_window_get_screen(window);
if (GDK_IS_X11_SCREEN(screen)) {
const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);
if (g_strcmp0(wm_name, "GNOME Shell") != 0) {
use_header_bar = FALSE;
}
}
#endif
if (use_header_bar) {
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
gtk_widget_show(GTK_WIDGET(header_bar));
gtk_header_bar_set_title(header_bar, "open_vinyl");
gtk_header_bar_set_show_close_button(header_bar, TRUE);
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
} else {
gtk_window_set_title(window, "open_vinyl");
}
gtk_window_set_default_size(window, 1280, 720);
g_autoptr(FlDartProject) project = fl_dart_project_new();
fl_dart_project_set_dart_entrypoint_arguments(
project, self->dart_entrypoint_arguments);
FlView* view = fl_view_new(project);
GdkRGBA background_color;
// Background defaults to black, override it here if necessary, e.g. #00000000
// for transparent.
gdk_rgba_parse(&background_color, "#000000");
fl_view_set_background_color(view, &background_color);
gtk_widget_show(GTK_WIDGET(view));
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
// Show the window when Flutter renders.
// Requires the view to be realized so we can start rendering.
g_signal_connect_swapped(view, "first-frame", G_CALLBACK(first_frame_cb),
self);
gtk_widget_realize(GTK_WIDGET(view));
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
gtk_widget_grab_focus(GTK_WIDGET(view));
}
// Implements GApplication::local_command_line.
static gboolean my_application_local_command_line(GApplication* application,
gchar*** arguments,
int* exit_status) {
MyApplication* self = MY_APPLICATION(application);
// Strip out the first argument as it is the binary name.
self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);
g_autoptr(GError) error = nullptr;
if (!g_application_register(application, nullptr, &error)) {
g_warning("Failed to register: %s", error->message);
*exit_status = 1;
return TRUE;
}
g_application_activate(application);
*exit_status = 0;
return TRUE;
}
// Implements GApplication::startup.
static void my_application_startup(GApplication* application) {
// MyApplication* self = MY_APPLICATION(object);
// Perform any actions required at application startup.
G_APPLICATION_CLASS(my_application_parent_class)->startup(application);
}
// Implements GApplication::shutdown.
static void my_application_shutdown(GApplication* application) {
// MyApplication* self = MY_APPLICATION(object);
// Perform any actions required at application shutdown.
G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application);
}
// Implements GObject::dispose.
static void my_application_dispose(GObject* object) {
MyApplication* self = MY_APPLICATION(object);
g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
}
static void my_application_class_init(MyApplicationClass* klass) {
G_APPLICATION_CLASS(klass)->activate = my_application_activate;
G_APPLICATION_CLASS(klass)->local_command_line =
my_application_local_command_line;
G_APPLICATION_CLASS(klass)->startup = my_application_startup;
G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown;
G_OBJECT_CLASS(klass)->dispose = my_application_dispose;
}
static void my_application_init(MyApplication* self) {}
MyApplication* my_application_new() {
// Set the program name to the application ID, which helps various systems
// like GTK and desktop environments map this running application to its
// corresponding .desktop file. This ensures better integration by allowing
// the application to be recognized beyond its binary name.
g_set_prgname(APPLICATION_ID);
return MY_APPLICATION(g_object_new(my_application_get_type(),
"application-id", APPLICATION_ID, "flags",
G_APPLICATION_NON_UNIQUE, nullptr));
}
+21
View File
@@ -0,0 +1,21 @@
#ifndef FLUTTER_MY_APPLICATION_H_
#define FLUTTER_MY_APPLICATION_H_
#include <gtk/gtk.h>
G_DECLARE_FINAL_TYPE(MyApplication,
my_application,
MY,
APPLICATION,
GtkApplication)
/**
* my_application_new:
*
* Creates a new Flutter-based application.
*
* Returns: a new #MyApplication.
*/
MyApplication* my_application_new();
#endif // FLUTTER_MY_APPLICATION_H_
+7
View File
@@ -0,0 +1,7 @@
# Flutter-related
**/Flutter/ephemeral/
**/Pods/
# Xcode-related
**/dgph
**/xcuserdata/
+1
View File
@@ -0,0 +1 @@
#include "ephemeral/Flutter-Generated.xcconfig"
+1
View File
@@ -0,0 +1 @@
#include "ephemeral/Flutter-Generated.xcconfig"
@@ -0,0 +1,14 @@
//
// Generated file. Do not edit.
//
import FlutterMacOS
import Foundation
import package_info_plus
import wakelock_plus
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin"))
}
+705
View File
@@ -0,0 +1,705 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXAggregateTarget section */
33CC111A2044C6BA0003C045 /* Flutter Assemble */ = {
isa = PBXAggregateTarget;
buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */;
buildPhases = (
33CC111E2044C6BF0003C045 /* ShellScript */,
);
dependencies = (
);
name = "Flutter Assemble";
productName = FLX;
};
/* End PBXAggregateTarget section */
/* Begin PBXBuildFile section */
331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; };
335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; };
33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; };
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 33CC10E52044A3C60003C045 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 33CC10EC2044A3C60003C045;
remoteInfo = Runner;
};
33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 33CC10E52044A3C60003C045 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 33CC111A2044C6BA0003C045;
remoteInfo = FLX;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
33CC110E2044A8840003C045 /* Bundle Framework */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Bundle Framework";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; };
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = "<group>"; };
33CC10ED2044A3C60003C045 /* open_vinyl.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "open_vinyl.app"; sourceTree = BUILT_PRODUCTS_DIR; };
33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = "<group>"; };
33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = "<group>"; };
33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = "<group>"; };
33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = "<group>"; };
33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = "<group>"; };
33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = "<group>"; };
33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = "<group>"; };
33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = "<group>"; };
33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
331C80D2294CF70F00263BE5 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
33CC10EA2044A3C60003C045 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
331C80D6294CF71000263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C80D7294CF71000263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
33BA886A226E78AF003329D5 /* Configs */ = {
isa = PBXGroup;
children = (
33E5194F232828860026EE4D /* AppInfo.xcconfig */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
333000ED22D3DE5D00554162 /* Warnings.xcconfig */,
);
path = Configs;
sourceTree = "<group>";
};
33CC10E42044A3C60003C045 = {
isa = PBXGroup;
children = (
33FAB671232836740065AC1E /* Runner */,
33CEB47122A05771004F2AC0 /* Flutter */,
331C80D6294CF71000263BE5 /* RunnerTests */,
33CC10EE2044A3C60003C045 /* Products */,
D73912EC22F37F3D000D13A0 /* Frameworks */,
);
sourceTree = "<group>";
};
33CC10EE2044A3C60003C045 /* Products */ = {
isa = PBXGroup;
children = (
33CC10ED2044A3C60003C045 /* open_vinyl.app */,
331C80D5294CF71000263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
33CC11242044D66E0003C045 /* Resources */ = {
isa = PBXGroup;
children = (
33CC10F22044A3C60003C045 /* Assets.xcassets */,
33CC10F42044A3C60003C045 /* MainMenu.xib */,
33CC10F72044A3C60003C045 /* Info.plist */,
);
name = Resources;
path = ..;
sourceTree = "<group>";
};
33CEB47122A05771004F2AC0 /* Flutter */ = {
isa = PBXGroup;
children = (
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */,
33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */,
33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */,
33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */,
);
path = Flutter;
sourceTree = "<group>";
};
33FAB671232836740065AC1E /* Runner */ = {
isa = PBXGroup;
children = (
33CC10F02044A3C60003C045 /* AppDelegate.swift */,
33CC11122044BFA00003C045 /* MainFlutterWindow.swift */,
33E51913231747F40026EE4D /* DebugProfile.entitlements */,
33E51914231749380026EE4D /* Release.entitlements */,
33CC11242044D66E0003C045 /* Resources */,
33BA886A226E78AF003329D5 /* Configs */,
);
path = Runner;
sourceTree = "<group>";
};
D73912EC22F37F3D000D13A0 /* Frameworks */ = {
isa = PBXGroup;
children = (
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C80D4294CF70F00263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
331C80D1294CF70F00263BE5 /* Sources */,
331C80D2294CF70F00263BE5 /* Frameworks */,
331C80D3294CF70F00263BE5 /* Resources */,
);
buildRules = (
);
dependencies = (
331C80DA294CF71000263BE5 /* PBXTargetDependency */,
);
name = RunnerTests;
productName = RunnerTests;
productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
33CC10EC2044A3C60003C045 /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
33CC10E92044A3C60003C045 /* Sources */,
33CC10EA2044A3C60003C045 /* Frameworks */,
33CC10EB2044A3C60003C045 /* Resources */,
33CC110E2044A8840003C045 /* Bundle Framework */,
3399D490228B24CF009A79C7 /* ShellScript */,
);
buildRules = (
);
dependencies = (
33CC11202044C79F0003C045 /* PBXTargetDependency */,
);
name = Runner;
productName = Runner;
productReference = 33CC10ED2044A3C60003C045 /* open_vinyl.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
33CC10E52044A3C60003C045 /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastSwiftUpdateCheck = 0920;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C80D4294CF70F00263BE5 = {
CreatedOnToolsVersion = 14.0;
TestTargetID = 33CC10EC2044A3C60003C045;
};
33CC10EC2044A3C60003C045 = {
CreatedOnToolsVersion = 9.2;
LastSwiftMigration = 1100;
ProvisioningStyle = Automatic;
SystemCapabilities = {
com.apple.Sandbox = {
enabled = 1;
};
};
};
33CC111A2044C6BA0003C045 = {
CreatedOnToolsVersion = 9.2;
ProvisioningStyle = Manual;
};
};
};
buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 33CC10E42044A3C60003C045;
productRefGroup = 33CC10EE2044A3C60003C045 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
33CC10EC2044A3C60003C045 /* Runner */,
331C80D4294CF70F00263BE5 /* RunnerTests */,
33CC111A2044C6BA0003C045 /* Flutter Assemble */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
331C80D3294CF70F00263BE5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
33CC10EB2044A3C60003C045 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */,
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
3399D490228B24CF009A79C7 /* ShellScript */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n";
};
33CC111E2044C6BF0003C045 /* ShellScript */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
Flutter/ephemeral/FlutterInputs.xcfilelist,
);
inputPaths = (
Flutter/ephemeral/tripwire,
);
outputFileListPaths = (
Flutter/ephemeral/FlutterOutputs.xcfilelist,
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
331C80D1294CF70F00263BE5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
33CC10E92044A3C60003C045 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */,
33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */,
335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
331C80DA294CF71000263BE5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 33CC10EC2044A3C60003C045 /* Runner */;
targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */;
};
33CC11202044C79F0003C045 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */;
targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
33CC10F42044A3C60003C045 /* MainMenu.xib */ = {
isa = PBXVariantGroup;
children = (
33CC10F52044A3C60003C045 /* Base */,
);
name = MainMenu.xib;
path = Runner;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
331C80DB294CF71000263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.openVinyl.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/open_vinyl.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/open_vinyl";
};
name = Debug;
};
331C80DC294CF71000263BE5 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.openVinyl.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/open_vinyl.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/open_vinyl";
};
name = Release;
};
331C80DD294CF71000263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.openVinyl.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/open_vinyl.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/open_vinyl";
};
name = Profile;
};
338D0CE9231458BD00FA5F75 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CODE_SIGN_IDENTITY = "-";
COPY_PHASE_STRIP = NO;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.15;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
};
name = Profile;
};
338D0CEA231458BD00FA5F75 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;
};
name = Profile;
};
338D0CEB231458BD00FA5F75 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Manual;
PRODUCT_NAME = "$(TARGET_NAME)";
};
name = Profile;
};
33CC10F92044A3C60003C045 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CODE_SIGN_IDENTITY = "-";
COPY_PHASE_STRIP = NO;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.15;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
33CC10FA2044A3C60003C045 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CODE_SIGN_IDENTITY = "-";
COPY_PHASE_STRIP = NO;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.15;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
};
name = Release;
};
33CC10FC2044A3C60003C045 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
};
name = Debug;
};
33CC10FD2044A3C60003C045 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;
};
name = Release;
};
33CC111C2044C6BA0003C045 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Manual;
PRODUCT_NAME = "$(TARGET_NAME)";
};
name = Debug;
};
33CC111D2044C6BA0003C045 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
PRODUCT_NAME = "$(TARGET_NAME)";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
331C80DB294CF71000263BE5 /* Debug */,
331C80DC294CF71000263BE5 /* Release */,
331C80DD294CF71000263BE5 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
33CC10F92044A3C60003C045 /* Debug */,
33CC10FA2044A3C60003C045 /* Release */,
338D0CE9231458BD00FA5F75 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
33CC10FC2044A3C60003C045 /* Debug */,
33CC10FD2044A3C60003C045 /* Release */,
338D0CEA231458BD00FA5F75 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = {
isa = XCConfigurationList;
buildConfigurations = (
33CC111C2044C6BA0003C045 /* Debug */,
33CC111D2044C6BA0003C045 /* Release */,
338D0CEB231458BD00FA5F75 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 33CC10E52044A3C60003C045 /* Project object */;
}
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
@@ -0,0 +1,99 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "open_vinyl.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "open_vinyl.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C80D4294CF70F00263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "open_vinyl.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "open_vinyl.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
+13
View File
@@ -0,0 +1,13 @@
import Cocoa
import FlutterMacOS
@main
class AppDelegate: FlutterAppDelegate {
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return true
}
override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
return true
}
}

Some files were not shown because too many files have changed in this diff Show More